./iris-lib, restore lib build stuff

This commit is contained in:
Martti Malmi 2022-09-22 10:29:50 +03:00
parent 117e2b1417
commit fb22e63931
60 changed files with 36652 additions and 37 deletions

28
iris-lib/.babelrc Normal file
View File

@ -0,0 +1,28 @@
{
"plugins": [
["transform-flow-strip-types"], ["transform-runtime"]
],
"env": {
"test": {
"presets": [
["es2015", {"loose": true}]
],
"plugins": [
["add-module-exports"]
]
},
"cjs": {
"presets": [
["es2015", {"loose": true}]
],
"plugins": [
["add-module-exports"], ["transform-runtime"]
]
},
"es": {
"presets": [
["es2015", {"loose": true, "modules": false}]
]
}
}
}

5
iris-lib/.eslintignore Normal file
View File

@ -0,0 +1,5 @@
cjs
es
dist
coverage
node_modules

12
iris-lib/.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
es
cjs
node_modules
coverage
*.log
test-report.html
jest-stare
.iris
.idea
.env
radata
*.key

35
iris-lib/.npmignore Normal file
View File

@ -0,0 +1,35 @@
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
node_modules
.DS_Store
__tests__
test-report.html
jest-stare
.github

122
iris-lib/README.md Normal file
View File

@ -0,0 +1,122 @@
# iris-lib
![Node](https://img.shields.io/node/v/iris-lib.svg?style=flat-square)
[![NPM](https://img.shields.io/npm/v/iris-lib.svg?style=flat-square)](https://www.npmjs.com/package/iris-lib)
[![Travis](https://img.shields.io/travis/irislib/iris-lib/master.svg?style=flat-square)](https://travis-ci.org/irislib/iris-lib)
[![David](https://img.shields.io/david/irislib/iris-lib.svg?style=flat-square)](https://david-dm.org/irislib/iris-lib)
[![Coverage Status](https://img.shields.io/coveralls/irislib/iris-lib.svg?style=flat-square)](https://coveralls.io/github/irislib/iris-lib)
[![Gitmoji](https://img.shields.io/badge/gitmoji-%20😜%20😍-FFDD67.svg?style=flat-square)](https://gitmoji.carloscuesta.me/)
<a href="https://opencollective.com/iris-social/donate" target="_blank"><img src="https://opencollective.com/iris-social/donate/button@2x.png?color=blue" width=200 /></a>
<p><sub>BTC donations: 3GopC1ijpZktaGLXHb7atugPj9zPGyQeST</sub></p>
### Description
Iris-lib allows you to integrate __decentralized social networking__ features into your application.
Public messaging: Add a troll-free comment box to your website or app.
Private chats: Don't reinvent the wheel - just deploy iris-lib for real-time private and group discussions. No phone number or other "account" needed - just generate a public key that your friends can optionally verify.
Web of trust: Filter out spam and other unwanted content, without giving power to central moderators. Iris public and private messages are automatically filtered. You can also filter your own datasets by user's web of trust distance.
Contacts management: Ask friends to verify your public key or cryptocurrency address and changes to them. Use verified payment addresses in crypto wallets. Use verified public keys for authentication instead of relying on centralized email addresses, domain names and passwords. Any other types of attributes can also be added and verified.
Iris-lib runs in the browser and on Node.js.
### Documentation
* [Iris API](http://docs.iris.to/)
* [Web components](https://examples.iris.to/components/)
### Example
Private channel:
```js
// Copy & paste this to console at https://iris.to or other page that has gun, sea and iris-lib
// Due to an unsolved bug, someoneElse's messages only start showing up after a reload
var gun1 = new Gun('https://gun-us.herokuapp.com/gun');
var gun2 = new Gun('https://gun-us.herokuapp.com/gun');
var myKey = await iris.Key.getDefault();
var someoneElse = localStorage.getItem('someoneElsesKey');
if (someoneElse) {
someoneElse = JSON.parse(someoneElse);
} else {
someoneElse = await iris.Key.generate();
localStorage.setItem('someoneElsesKey', JSON.stringify(someoneElse));
}
iris.Channel.initUser(gun1, myKey); // saves myKey.epub to gun.user().get('epub')
iris.Channel.initUser(gun2, someoneElse);
var ourChannel = new iris.Channel({key: myKey, gun: gun1, participants: someoneElse.pub});
var theirChannel = new iris.Channel({key: someoneElse, gun: gun2, participants: myKey.pub});
var myChannels = {}; // you can list them in a user interface
function printMessage(msg, info) {
console.log(`[${new Date(msg.time).toLocaleString()}] ${info.from.slice(0,8)}: ${msg.text}`)
}
iris.Channel.getChannels(gun1, myKey, channel => {
var pub = channel.getParticipants()[0];
gun1.user(pub).get('profile').get('name').on(name => channel.name = name);
myChannels[pub] = channel;
channel.getMessages(printMessage);
channel.on('mood', (mood, from) => console.log(from.slice(0,8) + ' is feeling ' + mood));
});
// you can play with these in the console:
ourChannel.send('message from myKey');
theirChannel.send('message from someoneElse');
ourChannel.put('mood', 'blessed');
theirChannel.put('mood', 'happy');
```
More examples: [tests](https://github.com/irislib/iris-lib/tree/master/__tests__)
### Tech
Data storage and networking are outsourced to [GUN](https://github.com/amark/gun), which manages the synchronization of data between different storages: RAM, localstorage, GUN websocket server, WebRTC peers, LAN multicast peers, IPFS (no adapter yet), S3 or others.
GUN enables subscription to data changes, so message feeds and contact details just update real-time without having to hit f5 or writing complex update logic.
IPFS is used to store file attachments and optional message backups.
### Installation
Install via [yarn](https://github.com/yarnpkg/yarn)
yarn add iris-lib (--dev)
or npm
npm install iris-lib (--save-dev)
### Builds
If you don't use a package manager, you can [access `iris-lib` via unpkg (CDN)](https://unpkg.com/iris-lib/), download the source, or point your package manager to the url.
`iris-lib` is compiled as a collection of [CommonJS](http://webpack.github.io/docs/commonjs.html) modules & [ES2015 modules](http://www.2ality.com/2014/09/es6-modules-final.html) for bundlers that support the `jsnext:main` or `module` field in package.json (Rollup, Webpack 2)
The `iris-lib` package includes precompiled production and development [UMD](https://github.com/umdjs/umd) builds in the [`dist` folder](https://unpkg.com/iris-lib/dist/). They can be used directly without a bundler and are thus compatible with many popular JavaScript module loaders and environments. You can drop a UMD build as a [`<script>` tag](https://unpkg.com/iris-lib) on your page. The UMD builds make `iris-lib` available as a `window.iris` global variable. Be sure to include [gun.js and sea.js](https://github.com/amark/gun) first.
```
<script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gun/sea.js"></script>
<script src="https://cdn.jsdelivr.net/npm/iris-lib@latest/dist/iris.min.js"></script>
```
### License
The code is available under the [MIT](LICENSE) license.
### Contributing
Please do **integrate** iris-lib with your existing application or with a test application and **create Github issues** for the bugs and other problems you may encounter. Your help is much appreciated!
TODO list is also available on [Trello](https://trello.com/b/8qUutkmP/iris).
[Majestic](https://github.com/Raathigesh/majestic) is a handy tool for viewing jest test results and coverage.
### Misc
This module was created using [generator-module-boilerplate](https://github.com/duivvv/generator-module-boilerplate).

View File

@ -0,0 +1,358 @@
const iris = require(`index.js`);
const GUN = require(`gun`);
const SEA = require(`gun/sea`);
const load = require(`gun/lib/load`);
const then = require(`gun/lib/then`);
const radix = require(`gun/lib/radix`); // Require before instantiating Gun, if running in jsdom mode
const server = require('http').createServer(GUN.serve);
const superNode = GUN({radisk: false, web: server.listen(8768), multicast: false });
const gun1 = new GUN({radisk: false, multicast: false, peers: ['http://localhost:8768/gun']});
const gun2 = new GUN({radisk: false, multicast: false, peers: ['http://localhost:8768/gun']});
const logger = function()
{
let oldConsoleLog = null;
const pub = {};
pub.enable = function enable()
{
if (oldConsoleLog == null)
return;
window[`console`][`log`] = oldConsoleLog;
};
pub.disable = function disable()
{
oldConsoleLog = console.log;
window[`console`][`log`] = function() {};
};
return pub;
}();
logger.disable();
test(`User1 says hi`, async (done) => {
const user1 = await iris.Key.generate();
const user2 = await iris.Key.generate();
const user2Channel = new iris.Channel({ gun: gun2, key: user2, participants: user1.pub });
const user1Channel = new iris.Channel({
gun: gun1,
key: user1,
participants: user2.pub
});
user1Channel.getMessages((msg, info) => {
expect(msg.text).toEqual(`hi`);
expect(info.selfAuthored).toBe(true);
done();
});
user1Channel.send(`hi`);
});
test(`Set and get msgsLastSeenTime`, async (done) => {
const user1 = await iris.Key.generate();
const user1Channel = new iris.Channel({
gun: gun1,
key: user1,
participants: user1.pub
});
const t = new Date();
user1Channel.setMyMsgsLastSeenTime();
user1Channel.getMyMsgsLastSeenTime(time => {
expect(time).toBeDefined();
expect(new Date(time).getTime()).toBeGreaterThanOrEqual(t.getTime());
done();
});
});
test(`User2 says hi`, async (done) => {
const user1 = await iris.Key.generate();
const user2 = await iris.Key.generate();
const user1Channel = new iris.Channel({
gun: gun1,
key: user1,
participants: user2.pub,
});
const user2Channel = new iris.Channel({ gun: gun2, key: user2, participants: user1.pub });
user2Channel.send(`hi`);
user1Channel.getMessages((msg) => {
if (msg.text === `hi`) {
done();
}
});
});
test(`3 users send and receive messages and key-value pairs on a group channel`, async (done) => {
const user1 = await iris.Key.generate();
const user2 = await iris.Key.generate();
const user3 = await iris.Key.generate();
iris.Channel.initUser(gun1, user1);
iris.Channel.initUser(gun2, user2);
iris.Channel.initUser(superNode, user3);
const user1Channel = new iris.Channel({
gun: gun1,
key: user1,
participants: [user2.pub, user3.pub]
});
expect(typeof user1Channel.uuid).toBe('string');
expect(typeof user1Channel.myGroupSecret).toBe('string');
expect(user1Channel.uuid.length).toBe(36);
user1Channel.send('1')
user1Channel.put('name', 'Champions');
const r1 = [];
const r2 = [];
const r3 = [];
let user1MsgsReceived, user2MsgsReceived, user3MsgsReceived, putReceived1, putReceived2, putReceived3;
function checkDone() {
if (user1MsgsReceived && user2MsgsReceived && user3MsgsReceived && putReceived1 && putReceived2 && putReceived3) {
done();
}
}
user1Channel.getMessages((msg) => {
r1.push(msg.text);
if (r1.indexOf('1') >= 0 && r1.indexOf('2') >= 0 && r1.indexOf('3') >= 0) {
user1MsgsReceived = true;
checkDone();
}
});
user1Channel.on('name', name => {
putReceived1 = name === 'Champions';
checkDone();
});
const user2Channel = new iris.Channel({ gun: gun2, key: user2, participants: [user1.pub, user3.pub], uuid: user1Channel.uuid });
user2Channel.send('2');
expect(user2Channel.uuid).toEqual(user1Channel.uuid);
expect(typeof user2Channel.myGroupSecret).toBe('string');
user2Channel.getMessages((msg) => {
r2.push(msg.text);
if (r2.indexOf('1') >= 0 && r2.indexOf('2') >= 0 && r2.indexOf('3') >= 0) {
user2MsgsReceived = true;
checkDone();
}
});
user2Channel.on('name', name => {
putReceived2 = name === 'Champions';
checkDone();
});
const user3Channel = new iris.Channel({ gun: superNode, key: user3, participants: [user1.pub, user2.pub], uuid: user1Channel.uuid });
user3Channel.send('3');
expect(user3Channel.uuid).toEqual(user1Channel.uuid);
expect(typeof user3Channel.myGroupSecret).toBe('string');
user3Channel.getMessages((msg) => {
r3.push(msg.text);
if (r3.indexOf('1') >= 0 && r3.indexOf('2') >= 0 && r3.indexOf('3') >= 0) {
user3MsgsReceived = true;
checkDone();
}
});
user3Channel.on('name', name => {
putReceived3 = name === 'Champions';
checkDone();
});
});
test(`create new channel, send messages, add participants afterwards`, async (done) => {
return done(); // temp disabled
logger.enable();
const user1 = await iris.Key.generate();
const user2 = await iris.Key.generate();
const user3 = await iris.Key.generate();
console.log(1, user1.pub.slice(0,4));
console.log(2, user2.pub.slice(0,4));
console.log(3, user3.pub.slice(0,4));
iris.Channel.initUser(gun1, user1);
iris.Channel.initUser(gun2, user2);
iris.Channel.initUser(superNode, user3);
const user1Channel = new iris.Channel({
gun: gun1,
key: user1,
participants: []
});
const chatLink = user1Channel.getSimpleLink();
expect(typeof user1Channel.uuid).toBe('string');
expect(typeof user1Channel.myGroupSecret).toBe('string');
expect(user1Channel.uuid.length).toBe(36);
user1Channel.send('1')
user1Channel.put('name', 'Champions');
user1Channel.addParticipant(user2.pub);
user1Channel.addParticipant(user3.pub);
const r1 = [];
const r2 = [];
const r3 = [];
let user1MsgsReceived, user2MsgsReceived, user3MsgsReceived, putReceived1, putReceived2, putReceived3;
function checkDone() {
if (user1MsgsReceived && user2MsgsReceived && user3MsgsReceived && putReceived1 && putReceived2 && putReceived3) {
logger.disable();
done();
}
}
user1Channel.getMessages((msg) => {
r1.push(msg.text);
console.log('user1', r1);
if (r1.indexOf('1') >= 0 && r1.indexOf('2') >= 0 && r1.indexOf('3') >= 0) {
user1MsgsReceived = true;
checkDone();
}
});
user1Channel.on('name', name => {
putReceived1 = name === 'Champions';
checkDone();
});
setTimeout(() => { // with separate gun instances would work without timeout?
const user2Channel = new iris.Channel({ gun: gun2, key: user2, chatLink });
user2Channel.send('2');
expect(user2Channel.uuid).toEqual(user1Channel.uuid);
expect(typeof user2Channel.myGroupSecret).toBe('string');
user2Channel.getMessages((msg) => {
r2.push(msg.text);
console.log('user2', r2);
if (r2.indexOf('1') >= 0 && r2.indexOf('2') >= 0 && r2.indexOf('3') >= 0) {
user2MsgsReceived = true;
checkDone();
}
});
user2Channel.on('name', name => {
putReceived2 = name === 'Champions';
checkDone();
});
}, 500);
setTimeout(() => {
const user3Channel = new iris.Channel({ gun: superNode, key: user3, chatLink });
user3Channel.send('3');
expect(user3Channel.uuid).toEqual(user1Channel.uuid);
expect(typeof user3Channel.myGroupSecret).toBe('string');
user3Channel.getMessages((msg) => {
r3.push(msg.text);
console.log('user3', r3);
if (r3.indexOf('1') >= 0 && r3.indexOf('2') >= 0 && r3.indexOf('3') >= 0) {
user3MsgsReceived = true;
checkDone();
}
});
user3Channel.on('name', name => {
putReceived3 = name === 'Champions';
checkDone();
});
}, 1000);
});
test(`Join a channel using a simple chat link`, async (done) => {
const user1 = await iris.Key.generate();
const user2 = await iris.Key.generate();
const user3 = await iris.Key.generate();
iris.Channel.initUser(gun1, user1);
iris.Channel.initUser(gun2, user2);
iris.Channel.initUser(superNode, user3);
const user1Channel = new iris.Channel({
gun: gun1,
key: user1,
participants: [user2.pub, user3.pub]
});
const chatLink = user1Channel.getSimpleLink();
setTimeout(() => {
const user2Channel = new iris.Channel({gun: gun2, key: user2, chatLink});
expect(user2Channel.uuid).toBe(user1Channel.uuid);
expect(Object.keys(user2Channel.participants).length).toBe(2);
user2Channel.onTheir('participants', pants => {
expect(typeof pants).toBe('object');
expect(Object.keys(pants).length).toBe(3);
expect(Object.keys(user2Channel.participants).length).toBe(3);
done();
});
}, 500);
});
test(`Retrieve chat links`, async (done) => {
const user1 = await iris.Key.generate();
iris.Channel.initUser(gun1, user1);
const user1Channel = new iris.Channel({
gun: gun1,
key: user1,
participants: []
});
const chatLink = await user1Channel.createChatLink();
user1Channel.getChatLinks(link => {
expect(link).toBeDefined();
expect(link.url).toEqual(chatLink);
done();
});
});
test(`Join a channel using an advanced chat link`, async (done) => {
logger.enable();
const user1 = await iris.Key.generate();
const user2 = await iris.Key.generate();
const user3 = await iris.Key.generate();
iris.Channel.initUser(gun1, user1);
iris.Channel.initUser(gun2, user2);
iris.Channel.initUser(superNode, user3);
const user1Channel = new iris.Channel({
gun: gun1,
key: user1,
participants: [user3.pub]
});
const chatLink = await user1Channel.createChatLink();
setTimeout(() => {
const user2Channel = new iris.Channel({gun: gun2, key: user2, chatLink});
console.log(1, chatLink);
console.log(2, user2Channel);
expect(user2Channel.uuid).toBe(user1Channel.uuid);
expect(Object.keys(user2Channel.participants).length).toBe(2);
user2Channel.onTheir('participants', pants => {
expect(typeof pants).toBe('object');
expect(Object.keys(pants).length).toBe(3);
expect(Object.keys(user2Channel.participants).length).toBe(3);
logger.disable();
done();
});
}, 500);
});
test(`Save and retrieve direct and group channels`, async (done) => {
const user1 = await iris.Key.generate();
const user2 = await iris.Key.generate();
iris.Channel.initUser(gun2, user2); // TODO direct chat is not saved unless the other guy's epub is found
const user3 = await iris.Key.generate();
const directChannel = new iris.Channel({
gun: gun1,
key: user1,
participants: user2.pub
});
const groupChannel = new iris.Channel({
gun: gun1,
key: user1,
participants: [user2.pub, user3.pub]
});
let direct, group;
iris.Channel.getChannels(gun1, user1, channel => {
if (channel.uuid) {
group = channel;
} else {
direct = channel;
}
if (group && direct) {
expect(direct.getId()).toBe(user2.pub);
expect(group.getId()).toBe(groupChannel.uuid);
expect(groupChannel.getCurrentParticipants().length).toBe(2);
expect(group.getCurrentParticipants().length).toBe(2);
done();
}
});
});

View File

@ -0,0 +1,105 @@
/*global describe, it, after, before */
const crypto = require(`crypto`);
const Attribute = require(`Attribute.js`);
const SignedMessage = require(`SignedMessage.js`);
const Contact = require(`Contact.js`);
const Key = require(`Key.js`);
jest.setTimeout(30000);
describe(`SignedMessage`, async () => {
let msg;
msg = void 0;
describe(`createRating method`, async () => {
test(`should create a rating message`, async () => {
msg = await SignedMessage.createRating({
author: {email: `alice@example.com`},
recipient: {email: `bob@example.com`},
rating: 5,
text: `Good guy`
});
expect(msg).toHaveProperty(`signedData.time`);
expect(msg.signedData.type).toEqual(`rating`);
});
/*
test('should get message author and recipient', async () => {
expect(msg.getAuthor()).toBeInstanceOf(Contact);
expect(msg.getRecipient()).toBeInstanceOf(Contact);
});
*/
test(`should use signing key as author if not defined`, async () => {
const defaultKey = await Key.getDefault(`.`, undefined, require('fs'));
msg = await SignedMessage.createRating({
recipient: {email: `bob@example.com`},
rating: 5,
text: `Good guy`
}, defaultKey);
expect(msg).toHaveProperty(`signedData.author`);
expect(JSON.stringify(msg.signedData.author)).toEqual(`{"keyID":"${ Key.getId(defaultKey) }"}`);
});
});
describe(`createVerification method`, async () => {
test(`should create a verification message`, async () => {
msg = await SignedMessage.createVerification({
author: {email: `alice@example.com`},
recipient: {email: `bob@example.com`, name: `Bob`},
text: `Good guy`
});
expect(msg).toHaveProperty(`signedData.time`);
expect(msg.signedData.type).toEqual(`verification`);
});
});
describe(`Recipient iterator`, async () => {
test(`should go over recipient attributes`, async () => {
msg = await SignedMessage.createVerification({
author: {email: `alice@example.com`},
recipient: {email: `bob@example.com`, name: `Bob`, nickname: [`Bobby`, `Bobbie`]},
text: `Good guy`
});
const seen = {};
for (const a of msg.getRecipientIterable()) {
seen[`${a.type }:${ a.value}`] = true;
}
expect(seen.hasOwnProperty(`email:bob@example.com`)).toBe(true);
expect(seen.hasOwnProperty(`name:Bob`)).toBe(true);
expect(seen.hasOwnProperty(`nickname:Bobby`)).toBe(true);
expect(seen.hasOwnProperty(`nickname:Bobbie`)).toBe(true);
});
});
describe(`Validation`, async () => {
test(`should not accept a message without signedData`, async () => {
const f = () => {
new SignedMessage({});
};
expect(f).toThrow(Error);
});
});
describe(`methods`, async () => {
let key;
msg = void 0;
beforeAll(async () => {
msg = await SignedMessage.createRating({
author: {email: `alice@example.com`},
recipient: {email: `bob@example.com`},
rating: 5,
text: `Good guy`
});
key = await Key.generate();
});
test(`should be signed with sign()`, async () => {
await msg.sign(key);
expect(msg).toHaveProperty(`sig`);
expect(msg).toHaveProperty(`pubKey`);
expect(msg).toHaveProperty(`hash`);
});
test(`should be verified with verify()`, async () => {
expect(await msg.verify()).toBe(true);
});
test(`serialize & deserialize`, async () => {
const h = msg.getHash();
const s = msg.toString();
const m = await SignedMessage.fromString(s);
expect(m.getHash()).toEqual(h);
});
});
});

View File

@ -0,0 +1,75 @@
const iris = require(`index.js`);
const Attribute = iris.Attribute;
describe(`Constructor`, () => {
test(`new Attribute(type, value)`, () => {
const a = new Attribute('email', 'alice@example.com');
expect(a.type).toBe('email');
expect(a.value).toBe('alice@example.com');
});
test(`new Attribute({type, value})`, () => {
const a = new Attribute({type: 'email', value: 'alice@example.com'});
expect(a.type).toBe('email');
expect(a.value).toBe('alice@example.com');
});
test(`new Attribute(value), recognized type`, () => {
const a = new Attribute('alice@example.com');
expect(a.type).toBe('email');
expect(a.value).toBe('alice@example.com');
});
test(`new Attribute(value) unrecognized type`, () => {
expect(() => new Attribute('#BADBAD;')).toThrow(Error);
});
test(`new Attribute(1, 'asdf') non-string 1st param`, () => {
expect(() => new Attribute(1, 'asdf')).toThrow(Error);
});
test(`new Attribute('asdf', 1) non-string 2nd param`, () => {
expect(() => new Attribute('asdf', 1)).toThrow(Error);
});
test(`new Attribute('', 'asdf') empty string 1st param`, () => {
expect(() => new Attribute('', 'asdf')).toThrow(Error);
});
test(`new Attribute('asdf', '') empty string 2nd param`, () => {
expect(() => new Attribute('asdf', '')).toThrow(Error);
});
});
describe(`equals`, () => {
test(`true`, () => {
const a = new Attribute('email', 'alice@example.com');
const b = new Attribute('email', 'alice@example.com');
expect(a.equals(b)).toBe(true);
expect(Attribute.equals(a, b)).toBe(true);
});
test(`false`, () => {
const a = new Attribute('email', 'alice@example.com');
const b = new Attribute('email', 'bob@example.com');
expect(a.equals(b)).toBe(false);
expect(Attribute.equals(a, b)).toBe(false);
expect(a.equals({})).toBe(false);
});
});
describe(`static methods`, () => {
test(`guessTypeOf`, () => {
expect(Attribute.guessTypeOf('bob@example.com')).toBe('email');
expect(Attribute.guessTypeOf('#BADBAD;')).toBe(undefined);
});
test(`getUniqueIdValidators`, () => {
expect(typeof Attribute.getUniqueIdValidators()).toBe(`object`);
expect(Object.keys(Attribute.getUniqueIdValidators()).length).toBeGreaterThan(10);
})
});
describe(`methods`, () => {
/*
test(`identicon()`, () => {
const a = new Attribute('a', 'b');
const identicon = a.identicon(50);
expect(a.identicon(50).constructor.name).toBe(`HTMLDivElement`);
});
*/
test(`getUuid()`, () => {
const uuid = Attribute.getUuid();
expect(uuid.type).toBe(`uuid`);
expect(typeof uuid.value).toBe(`string`);
expect(uuid.value.length).toBeGreaterThan(10);
})
});

View File

@ -0,0 +1,95 @@
const Collection = require(`Collection.js`);
const Gun = require(`gun`);
const open = require(`gun/lib/open`);
const radix = require(`gun/lib/radix`); // Require before instantiating Gun, if running in jsdom mode
class Animal {
constructor(name, species) {
if (!name || name.length === 0) { throw new Error(`Invalid name`); }
if (!species || species.length === 0) { throw new Error(`Invalid species`); }
this.name = name;
this.species = species;
}
serialize() {
return {name: this.name, species: this.species};
}
static deserialize(data) {
return new Animal(data.name, data.species);
}
}
describe(`Collection`, () => {
let gun, animals, n = 0;
beforeAll(() => {
gun = new Gun({radisk: false});
const indexer = animal => {
const reversedName = animal.name.split(``).reverse().join(``).toLowerCase();
return {reversedName};
};
animals = new Collection({gun, class: Animal, indexes: ['name', 'species'], indexer});
});
test(`put`, () => {
animals.put(new Animal('Moisture', 'cat'));n++;
animals.put(new Animal('Petard', 'cat'));n++;
animals.put(new Animal('Petunia', 'cat'));n++;
animals.put(new Animal('Petrol', 'cat'));n++;
animals.put(new Animal('Parsley', 'cat'));n++;
animals.put(new Animal('proton', 'cat'));n++;
animals.put(new Animal('Oilbag', 'cat'));n++;
animals.put(new Animal('Scumbag', 'dog'));n++;
animals.put(new Animal('Deadbolt', 'parrot'));n++;
animals.put(new Animal('Moisture', 'dog'));n++;
});
test(`get all`, done => {
let timesCalled = 0;
function callback(animal) {
expect(animal).toBeInstanceOf(Animal);
timesCalled++;
if (timesCalled === n) {
done();
}
}
animals.get({callback});
});
test(`search (case sensitive)`, done => {
let timesCalled = 0;
function callback(animal) {
expect(animal).toBeInstanceOf(Animal);
expect(animal.name.indexOf('P')).toBe(0);
timesCalled++;
if (timesCalled === 4) {
done();
}
}
animals.get({callback, query: {name: 'P'}});
});
test(`search (case insensitive)`, done => {
let timesCalled = 0;
function callback(animal) {
expect(animal).toBeInstanceOf(Animal);
expect(animal.name.toLowerCase().indexOf('p')).toBe(0);
timesCalled++;
if (timesCalled === 5) {
done();
}
}
animals.get({callback, query: {name: 'P'}, caseSensitive: false});
});
test(`selector`, done => {
let timesCalled = 0;
function callback(animal) {
expect(animal).toBeInstanceOf(Animal);
expect(animal.name).toBe(`Moisture`);
timesCalled++;
if (timesCalled === 2) {
done();
}
}
animals.get({callback, selector: {name: 'Moisture'}});
});
// TODO: test multiple index search
// TODO: unique vs non-unique indexes
// TODO: delete from collection and indexes
});

View File

@ -0,0 +1,67 @@
const iris = require('index.js');
const Contact = iris.Contact;
const GUN = require(`gun`);
const load = require(`gun/lib/load`);
const then = require(`gun/lib/then`);
const SEA = require(`gun/sea`);
const radix = require(`gun/lib/radix`); // Require before instantiating Gun, if running in jsdom mode
const gun = new GUN({radisk: false});
const $ = require(`jquery`);
const logger = function()
{
let oldConsoleLog = null;
const pub = {};
pub.enable = function enable()
{
if (oldConsoleLog == null)
return;
window[`console`][`log`] = oldConsoleLog;
};
pub.disable = function disable()
{
oldConsoleLog = console.log;
window[`console`][`log`] = function() {};
};
return pub;
}();
// get them from index, actually?
let index, vp;
beforeAll(async () => {
logger.disable();
index = new iris.SocialNetwork({gun});
await index.ready;
vp = index.getRootContact();
});
describe(`Contact`, () => {
test(`verified()`, async () => {
expect(await vp.verified(`email`)).toBe(undefined);
expect(typeof await vp.verified(`keyID`)).toBe(`string`);
});
test(`identicon()`, () => {
const identicon = vp.identicon(50);
expect(identicon.constructor.name).toBe(`HTMLDivElement`);
});
test(`profileCard()`, () => {
const profileCard = vp.profileCard();
expect(profileCard.constructor.name).toBe(`HTMLDivElement`);
});
test(`appendSearchWidget()`, () => {
const parent = document.createElement(`div`); // index param
const widget = Contact.appendSearchWidget(parent);
expect(parent.hasChildNodes()).toBe(true);
const input = $(widget).find(`input`).first();
const results = $(widget).find(`div`).first();
expect(input.constructor.name).toBe(`jQuery`);
expect(results.constructor.name).toBe(`jQuery`);
input.val(`Al`);
input.keyup();
});
});

41
iris-lib/__tests__/key.js Normal file
View File

@ -0,0 +1,41 @@
const iris = require(`index.js`);
const fs = require(`fs`);
jest.setTimeout(30000);
beforeAll(() => {
if (fs.existsSync(`./private.key`)) {
const f = fs.unlinkSync(`./private.key`);
}
});
test(`Generate key`, async () => {
const i = await iris.Key.generate();
expect(i).toBeDefined();
});
test(`Serialize and deserialize a key`, async () => {
const i = await iris.Key.generate();
const serialized = iris.Key.toString(i);
expect(typeof serialized).toBe(`string`);
const deserialized = iris.Key.fromString(serialized);
expect(typeof deserialized).toBe(`object`);
expect(i).toBeDefined();
});
test(`Get default key and sign a message with it`, async () => {
const i = await iris.Key.getDefault(`.`, undefined, fs);
expect(i).toBeDefined();
const j = await iris.Key.getDefault(`.`, undefined, fs);
expect(i).toEqual(j);
const msg = await iris.SignedMessage.createRating({
author: {email: `alice@example.com`},
recipient: {email: `bob@example.com`},
rating: 5,
comment: `Good guy`
});
await msg.sign(i);
expect(await msg.verify()).toBe(true);
});
afterAll(() => {
if (fs.existsSync(`./private.key`)) {
const f = fs.unlinkSync(`./private.key`);
}
});

8159
iris-lib/dist/iris.js vendored Normal file

File diff suppressed because one or more lines are too long

1
iris-lib/dist/iris.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,65 @@
const identifi = require('../cjs/index.js');
const IPFS = require('ipfs');
const fs = require('fs');
let key, ipfsNode;
async function init() {
key = identifi.Key.getDefault();
ipfsNode = new IPFS({repo: './ipfs_repo'});
await new Promise((resolve, reject) => {
ipfsNode.on('ready', () => {
console.log('ipfs ready');
resolve();
});
ipfsNode.on('error', error => {
console.error(error.message);
reject();
});
});
return true;
}
function shuffle(array) {
let currentIndex = array.length, temporaryValue, randomIndex;
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
};
init().then(async () => {
console.log(1);
const i = await identifi.Index.create(ipfsNode);
console.log(11);
const msgs = [];
let msg = identifi.Message.createRating({recipient: [['email', 'bob@example.com']], rating:10}, key);
msgs.push(msg);
msg = identifi.Message.createRating({author: [['email', 'bob@example.com']], recipient: [['email', 'bob1@example.com']], rating:10}, key);
msgs.push(msg);
for (let i = 0;i < 10;i++) {
msg = identifi.Message.createRating({author: [['email', `bob${i}@example.com`]], recipient: [['email', `bob${i+1}@example.com`]], rating:10}, key);
msgs.push(msg);
}
msg = identifi.Message.createRating({author: [['email', 'bert@example.com']], recipient: [['email', 'chris@example.com']], rating:10}, key);
msgs.push(msg);
await i.addMessages(shuffle(msgs));
p = await i.get('bob10@example.com');
console.log(111,p);
p = await i.get('bert@example.com');
console.log(1111,p);
p = await i.get('chris@example.com');
console.log(11111,p);
await ipfsNode.stop();
console.log('ipfsNode stopped');
});

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,53 @@
var Gun = (typeof window !== "undefined")? window.Gun : require('../gun');
Gun.chain.open = function(cb, opt, at){
opt = opt || {};
opt.doc = opt.doc || {};
opt.ids = opt.ids || {};
opt.any = opt.any || cb;
opt.ev = opt.ev || {off: function(){
Gun.obj.map(opt.ev.s, function(e){
if(e){ e.off() }
});
opt.ev.s = {};
}, s:{}}
return this.on(function(data, key, ctx, ev){
delete ((data = Gun.obj.copy(data))||{})._;
clearTimeout(opt.to);
opt.to = setTimeout(function(){
if(!opt.any){ return }
opt.any.call(opt.at.$, opt.doc, opt.key, opt, opt.ev);
if(opt.off){
opt.ev.off();
opt.any = null;
}
}, opt.wait || 1);
opt.at = opt.at || ctx;
opt.key = opt.key || key;
opt.ev.s[this._.id] = ev;
if(Gun.val.is(data)){
if(!at){
opt.doc = data;
} else {
at[key] = data;
}
return;
}
var tmp = this, id;
Gun.obj.map(data, function(val, key){
var doc = at || opt.doc;
if (!doc) {
return;
}
if(!(id = Gun.val.link.is(val))){
doc[key] = val;
return;
}
if(opt.ids[id]){
doc[key] = opt.ids[id];
return;
}
tmp.get(key).open(opt.any, opt, opt.ids[id] = doc[key] = {});
});
})
}

View File

@ -0,0 +1,21 @@
var Gun = (typeof window !== "undefined")? window.Gun : require('../gun');
// Returns a gun reference in a promise and then calls a callback if specified
Gun.chain.promise = function(cb) {
var gun = this, cb = cb || function(ctx) { return ctx };
return (new Promise(function(res, rej) {
gun.once(function(data, key){
res({put: data, get: key, gun: this}); // gun reference is returned by promise
});
})).then(cb); //calling callback with resolved data
};
// Returns a promise for the data, key of the gun call
Gun.chain.then = function() {
var gun = this;
return (new Promise((res, rej)=>{
gun.once(function (data, key) {
res(data, key); //call resolve when data is returned
})
}))
};

View File

@ -0,0 +1,112 @@
<html>
<head>
<title>identifi-lib</title>
<!--<script type="text/javascript" src="../dist/iris.js"></script>-->
<script type="text/javascript" src="./assets/gun.js"></script>
<script type="text/javascript" src="./assets/gun.then.js"></script>
<script type="text/javascript" src="./assets/gun.open.js"></script>
<script type="text/javascript" src="../dist/iris.js"></script>
<script src="script.js"></script>
<link rel="stylesheet" href="./assets/bootstrap.min.css">
</head>
<body>
<div class="container">
<h1>Saving the world with identifi-lib</h1>
<p>The building blocks of Identifi data are digitally signed <b>Messages</b>
which are typically <i>verifications</i> or <i>ratings</i>.</p>
<p>Ratings are used to build a <i>web of trust</i>. Your web of trust consists
of the identities that you have rated positively, the identities that they have rated
positively and so on, up to an arbitrary <i>trust distance</i>.</p>
<p>Verifications are claims that certain <b>Attributes</b>
(name, email, bitcoin address etc.) belong to the same <b>Identity</b>.
Verifications from your web of trust are used to bundle together
attributes into Identity objects.</p>
<p>Identifi-lib can be used to read and write <b>Indices</b> of Messages and
Identities. An Identifi Index typically contains four indices:
<i>messagesByTimestamp</i>, <i>messagesByTrustDistance</i>,
<i>identitiesByTrustDistance</i> and <i>identitiesBySearchKey</i>.</p>
<p>Identifi messages and indexes are stored on GUN. Additionally, messages are saved to IPFS.</p>
<h2>Including the library</h2>
<h3>Browser</h3>
<pre>&lt;script type="text/javascript" src="https://unpkg.com/identifi-lib/dist/iris.min.js">&lt;/script></pre>
<p>The lib will be available as <i>window.irisLib</i>.</p>
<h3>Node.js</h3>
<pre>npm install identifi-lib</pre>
<!--
<h2>index = await Index.load(indexRootIpfsUri)</h2>
<p>Load an Index from a given index root IPFS/IPNS URI. If ipfsUri is undefined,
identi.fi index is loaded by default. As of April 2018, identi.fi index consists
mostly of data crawled from bitcoin-otc.com.</p>
-->
<h2>index.search(value, type, callback, limit)</h2>
<p>Search an identifi index for identitiesBySearchKey. Callback is called for each result.</p>
<input type="text" value="ma" id="query" onkeyup="search()" class="form-control">
<p id="searchResults"></p>
<p>You can create a similar search widget with:</p>
<pre>window.irisLib.Identity.appendSearchWidget(parentElement, index);</pre>
<p id="searchWidget"></p>
<h2>index.get([type,] value)</h2>
<p>Search the identitiesBySearchKey index using type and value. If an exact
match is found, the resulting Identity object is returned. If type is not defined,
it will be guessed using a regex list.</p>
<p><input type="text" value="martti@moni.com" id="profileQuery" onkeyup="getProfile()" class="form-control"></p>
<textarea style="width:100%" rows=10 id="profileResults" class="form-control"></textarea>
<h3>identity.identicon(100)</h3>
<p>Generate identicon of width 100px:</p>
<p id="identicon"></p>
<h3>identity.verified('name')</h3>
<p>Get the most verified attribute of type 'name':</p>
<p>Result: <b id="verifiedAttribute"></b></p>
<h2>Create, sign and publish a message</h2>
<pre>msg = await Message.createRating(</pre>
<textarea id="ratingMsg" style="width: 100%" rows=12 class="form-control">
{
"author": {
name: "Alice",
email: "alice@example.com"
},
"recipient": {
name: "Bob",
keyID: "4321DCBA"
},
"rating": 10,
"comment": "Good"
}</textarea>
<pre>
);
key = await util.getDefaultKey();</pre>
<p><input type="button" value="msg.sign(key);" id="signMsg" class="btn btn-primary"></p>
<pre id="signMsgResult"></pre>
<p><input type="button" value="index.addMessage(msg);" id="publishMsg" class="btn btn-primary"></p>
<pre id="publishMsgResult"></pre>
<p id="publishMsgResultLink"></p>
<p>Possible applications:</p>
<ul>
<li>Contact details search in any application</li>
<li>Finding payment addresses in crypto wallets</li>
<li>Finding public keys of other users in encrypted instant messaging apps</li>
<li>Finding network addresses of servers (instead of using DNS)</li>
<li>Filter out untrusted content in decentralized social media</li>
<li>Browser plugin to filter the content in existing social media</li>
<li>Email spam filtering</li>
<li>Filter reviews on marketplace platforms (Airbnb, eBay, Uber, LocalBitcoins)</li>
<li>Uncensorable platforms for dispute announcement and resolution</li>
</ul>
</div>
</body>
</html>

View File

@ -0,0 +1,19 @@
<html>
<head>
<title>Live Chat Example</title>
<meta name="viewport" content="width=device-width, user-scalable=no">
<script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
<script src="./sea.js"></script>
<script src="../iris.js"></script>
</head>
<body style="background: #c5c7f7">
<script type="text/javascript">
iris.Key.getDefault().then(key => {
var gun = new Gun({peers: ['https://gun-eu.herokuapp.com/gun']});
// replace chatLink with your own chat link from iris.to
var chatLink = 'https://iris.to/?chatWith=4JhaYuPVcq4y2Sp6sRAGkbwM5FdhsMih3b4E6tvd5W4.ULpD5dhra5ojHtKFEdcTZ80UZEmZnRl4dfM2JCEzj2M&s=ZaUbkxPsQeSSdSP1ety7y19eTjPq1gHu15s1v8cbGX4&k=26vMMto5xufO';
iris.Chat.addChatButton({label: 'Live Chat', chatOptions: {gun, key, chatLink}});
});
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,127 @@
<!doctype html>
<html>
<head>
<title>Iris web components</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gun/sea.js"></script>
<script src="../iris.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
</head>
<body>
<div class="jumbotron">
<div class="container">
<h1 class="display-1">Iris web components</h1>
<p class="lead">
Just include iris, gun and sea, and these web components are available on your page.
</p>
<pre>
&lt;script src="https://cdn.jsdelivr.net/npm/gun/gun.js">&lt;/script>
&lt;script src="https://cdn.jsdelivr.net/npm/gun/sea.js">&lt;/script>
&lt;script src="https://cdn.jsdelivr.net/npm/iris-lib/dist/iris.js">&lt;/script>
</pre>
<p><a href="https://github.com/irislib/iris-lib">GitHub</a></p>
</div>
</div>
<div class="container">
<h2>Text node</h2>
<p>
Get a text node, optionally editable, from <code>gun.user(pub)</code> space. Useful for fetching profile attributes and other text fields.
</p>
<p>
<code>&lt;iris-text path="profile/name" user="hyECQHwSo7fgr2MVfPyakvayPeixxsaAWVtZ-vbaiSc.TXIp8MnCtrnW6n2MrYquWPcc-DTmZzMBmc2yaGv9gIU"/></code>
</p>
<p>
<iris-text path="profile/name" user="hyECQHwSo7fgr2MVfPyakvayPeixxsaAWVtZ-vbaiSc.TXIp8MnCtrnW6n2MrYquWPcc-DTmZzMBmc2yaGv9gIU"/>
</p>
<p>
<code>&lt;iris-text path="profile/about" user="hyECQHwSo7fgr2MVfPyakvayPeixxsaAWVtZ-vbaiSc.TXIp8MnCtrnW6n2MrYquWPcc-DTmZzMBmc2yaGv9gIU"/></code>
</p>
<p>
<iris-text path="profile/about" user="hyECQHwSo7fgr2MVfPyakvayPeixxsaAWVtZ-vbaiSc.TXIp8MnCtrnW6n2MrYquWPcc-DTmZzMBmc2yaGv9gIU"/>
</p>
<p>For currently logged in user attribute, leave user blank: <code>user=""</code>. It will be contenteditable, unless you add <code>editable="false"</code>.</p>
<p>
<code>&lt;iris-text placeholder="What's your name?" user="" path="profile/name"/></code><br/>
<code>Hello &lt;b>&lt;iris-text placeholder="name" user="" editable="false" path="profile/name"/>&lt;/b>!</code>
</p>
<p>
<iris-text placeholder="What's your name?" user="" path="profile/name"/>
<br/>
Hello <b><iris-text placeholder="name" user="" editable="false" path="profile/name"></iris-text></b>!
</p>
<p>To parse user input as JSON, add <code>json="true"</code>.</p>
<h2>Image node</h2>
<p>Same as text node, but stores a base64 image. If user === loggedInUser, you can click the photo to change it.</p>
<p>
<code>&lt;iris-img btn-class="btn btn-primary" path="profile/photo"/></code>
</p>
<p>
<iris-img btn-class="btn btn-primary" path="profile/photo"/>
</p>
<h2>Identicon</h2>
<p>
<code>&lt;iris-identicon user="hyECQHwSo7fgr2MVfPyakvayPeixxsaAWVtZ-vbaiSc.TXIp8MnCtrnW6n2MrYquWPcc-DTmZzMBmc2yaGv9gIU" width="100"/></code>
</p>
<p>
<iris-identicon user="hyECQHwSo7fgr2MVfPyakvayPeixxsaAWVtZ-vbaiSc.TXIp8MnCtrnW6n2MrYquWPcc-DTmZzMBmc2yaGv9gIU" width="100"/>
</p>
<h2>Copy button</h2>
<p>
<code>&lt;iris-copy-button inner-class="btn btn-primary" str="https://iris.to/#/profile/hyECQHwSo7fgr2MVfPyakvayPeixxsaAWVtZ-vbaiSc.TXIp8MnCtrnW6n2MrYquWPcc-DTmZzMBmc2yaGv9gIU"/></code>
</p>
<p>
<iris-copy-button inner-class="btn btn-primary" str="https://iris.to/#/profile/hyECQHwSo7fgr2MVfPyakvayPeixxsaAWVtZ-vbaiSc.TXIp8MnCtrnW6n2MrYquWPcc-DTmZzMBmc2yaGv9gIU"/>
</p>
<h2>Follow button</h2>
<p>
<code>&lt;iris-follow-button inner-class="btn btn-primary" user="hyECQHwSo7fgr2MVfPyakvayPeixxsaAWVtZ-vbaiSc.TXIp8MnCtrnW6n2MrYquWPcc-DTmZzMBmc2yaGv9gIU"/></code>
</p>
<p>
<iris-follow-button inner-class="btn btn-primary" user="hyECQHwSo7fgr2MVfPyakvayPeixxsaAWVtZ-vbaiSc.TXIp8MnCtrnW6n2MrYquWPcc-DTmZzMBmc2yaGv9gIU"/>
</p>
<h2>Search</h2>
<p>
<code>&lt;iris-search inner-class="form-control"/></code>
<pre>
&lt;script type="text/javascript">
window.onIrisSearchSelect = item => doSomething(item);
&lt;/script>
</pre>
</p>
<p>
<iris-search id="search" inner-class="form-control"/>
<script type="text/javascript">
window.onIrisSearchSelect = i => {
console.log(i);
const identicon = document.querySelector('#selected-result-identicon');
identicon.style.display = '';
identicon.setAttribute('user', i.key);
const nameEl = document.querySelector('#selected-result-name');
nameEl.style.display = '';
nameEl.setAttribute('user', i.key);
document.querySelector('#search-result-raw').innerHTML = JSON.stringify(i);
}
</script>
</p>
<h3>
<iris-text style="display:none" id="selected-result-name" path="profile/name"/>
</h3>
<p>
<iris-identicon style="display: none;" id="selected-result-identicon" width="100"/>
</p>
<pre id="search-result-raw"></pre>
<hr style="margin-bottom:500px" />
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

11881
iris-lib/example/iris.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,15 @@
const identifi = require('../cjs/');
identifi.Index.load().then(async (index) => {
let r = await index.search('ma');
console.log('Search results for "ma":');
console.log(JSON.stringify(r, null, 2));
console.log();
console.log('Identity profile for martti@moni.com:');
r = await index.get('martti@moni.com');
console.log(JSON.stringify(r, null, 2));
console.log();
console.log('The most verified name for martti@moni.com:');
console.log(JSON.stringify(r.verified('name'), null, 2));
})
;

113
iris-lib/example/script.js Normal file
View File

@ -0,0 +1,113 @@
let index, msg, key;
gun = new Gun(['http://localhost:8765/gun']);
index = new window.irisLib.Index({gun}); // <--- Create identifi index
index.ready.then(async () => {
document.getElementById('searchResults').textContent = 'Searching...';
document.getElementById('profileResults').textContent = 'Searching...';
await search(index);
await getProfile(index);
document.getElementById('query').addEventListener('change', search);
document.getElementById('profileQuery').addEventListener('change', getProfile);
document.getElementById('signMsg').addEventListener('click', signMsg);
document.getElementById('publishMsg').addEventListener('click', publishMsg);
document.getElementById('runIndexExample').addEventListener('click', runIndexExample);
const searchWidget = document.getElementById('searchWidget');
window.irisLib.Identity.appendSearchWidget(searchWidget, index);
});
async function search() {
const query = document.getElementById('query').value;
let r = await index.search(query); // <--- Search identities from identifi
let text = `Search results for "${query}":\n`;
document.getElementById('searchResults').textContent = text;
r.sort((a, b) => {return a.trustDistance - b.trustDistance;});
r.forEach(i => {
console.log('ii', i);
document.getElementById('searchResults').appendChild(i.profileCard());
});
}
async function getProfile() {
const profileQuery = document.getElementById('profileQuery').value;
const identiconParent = document.getElementById('identicon');
const verifiedAttributeEl = document.getElementById('verifiedAttribute');
i = await index.get(profileQuery); // <--- Get an identity from identifi
let text = `Identity profile for ${profileQuery}:\n`;
if (i) {
const data = await new Promise(resolve => i.gun.load(r => resolve(r)));
text += JSON.stringify(data, null, 2);
const verifiedName = await i.verified('name'); // <--- Get a verified name of an identity
verifiedAttributeEl.textContent = verifiedName;
console.log('i', i);
const identicon = i.identicon(100); // <--- Generate an identity icon as a DOM element, width 100px
identiconParent.innerHTML = '';
identiconParent.appendChild(identicon);
} else {
identiconParent.innerHTML = '';
verifiedAttributeEl.innerHTML = '-';
text += 'Profile not found'
}
document.getElementById('profileResults').textContent = text;
}
function signMsg() {
const d = document.getElementById('ratingMsg').value;
const msgData = JSON.parse(d);
window.message = window.irisLib.Message.createRating(msgData); // <--- Create an Identifi message
key = window.irisLib.Key.getDefault(); // <--- Get or generate local key
window.message.sign(key); // <--- Sign message with the key
document.getElementById('signMsgResult').textContent = JSON.stringify(window.message, null, 2);
}
async function publishMsg() {
const r = await index.addMessage(window.message);
document.getElementById('publishMsgResult').textContent = JSON.stringify(r);
if (r && r.hash) {
const link = `https://ipfs.io/ipfs/${r.hash}`;
const el = document.getElementById('publishMsgResultLink');
el.className = `alert alert-info`;
el.innerHTML = `<a href="${link}">${link}</a>`;
}
}
async function runIndexExample() {
el = document.getElementById('runIndexExampleResult');
el.innerHTML = '';
ipfs = new window.Ipfs();
await new Promise((resolve, reject) => {
ipfs.on('ready', () => {
console.log('ipfs ready');
resolve();
});
ipfs.on('error', error => {
console.error(error.message);
reject();
});
});
index = new window.irisLib.Index({gun, ipfs});
myKey = window.irisLib.Key.getDefault('.');
msg = window.irisLib.Message.createVerification({
recipient: {'keyID': myKey.keyID, 'name': 'Alice Example'},
comment: 'add name'
}, myKey);
await index.addMessage(msg);
msg2 = window.irisLib.Message.createRating({
recipient: {'email': 'bob@example.com'},
rating: 5
}, myKey);
await index.addMessage(msg2);
identities = await index.search('');
el.innerHTML += 'Identities in index:\n'
el.innerHTML += JSON.stringify(identities, null, 2);
uri = await index.save();
el.innerHTML += '\nSaved index URI: ' + uri
}

135
iris-lib/package.json Normal file
View File

@ -0,0 +1,135 @@
{
"name": "iris-lib",
"version": "0.0.159",
"description": "Basic tools for reading and writing Iris messages and identities.",
"main": "src/index.js",
"react-native": {
"fs": false
},
"browser": "dist/iris.js",
"module": "es/index.js",
"jsxnext:main": "es/index.js",
"jest": {
"testURL": "http://localhost/",
"bail": true,
"forceExit": true,
"testEnvironment": "jsdom",
"verbose": true,
"collectCoverage": true,
"watchPathIgnorePatterns": [
"jest-results.json",
"jest-stare.*",
""
],
"reporters": [
"default",
[
"./node_modules/jest-html-reporter",
{
"pageTitle": "Test Report",
"includeFailureMsg": true,
"includeConsoleLog": true
}
],
"jest-stare"
],
"testPathIgnorePatterns": [
"/node_modules/",
"/yarn_dev/",
"/yarn_prod/"
],
"modulePaths": [
"<rootDir>/node_modules/",
"<rootDir>/cjs/"
]
},
"files": [
"dist",
"cjs",
"es",
"src"
],
"engines": {
"node": ">=10.0.0"
},
"scripts": {
"release": "release-it",
"coveralls": "cat ./coverage/lcov.info | ./node_modules/.bin/coveralls",
"lint": "yarn run lint:eslint",
"lint:eslint": "eslint src/*.js",
"lint:flow": "flow --color always",
"test": "clear && jest",
"test:coverage": "jest --coverage ",
"test:watch": "clear && yarn build:cjs && jest --watchAll",
"test:debug": "node --inspect node_modules/.bin/jest --runInBand",
"test:all": "yarn lint:test && yarn lint && yarn test:coverage --runInBand",
"lint:test": "yarn run lint && npm run test:coverage",
"build": "clear && npm run build:cjs && npm run lint:test && npm run build:es && npm run build:umd && npm run build:docs",
"build:watch": "clear && rimraf cjs && cross-env BABEL_ENV=cjs babel -w src --out-dir cjs",
"build:es": "rimraf es && cross-env BABEL_ENV=es babel src --out-dir es",
"build:cjs": "rimraf cjs && cross-env BABEL_ENV=cjs babel src --out-dir cjs",
"build:umd": "rimraf dist && cross-env BABEL_ENV=es rollup -c & cross-env BABEL_ENV=es NODE_ENV=production rollup -c",
"build:umdev": "rimraf dist && cross-env BABEL_ENV=es rollup -c",
"build:docs": "documentation build src/** -f html -o docs --config documentation.yml",
"build:run": "clear && rimraf cjs && cross-env BABEL_ENV=cjs yarn nodemon src/server.js --watch src --exec babel-node",
"dev": "concurrently -n build,tests \"yarn build:watch\" \"yarn test:watch\"",
"dev:server": "concurrently -n build,tests,server \"yarn build:watch\" \"yarn test:watch\" \"yarn build:run\"",
"serve": "node cjs/server.js",
"start": "node cjs/server.js",
"heroku-postbuild": "echo \"not running build script\""
},
"keywords": [],
"author": "Martti Malmi (martti.malmi@iki.fi)",
"license": "MIT",
"repository": "https://github.com/irislib/iris-lib",
"homepage": "https://github.com/irislib/iris-lib",
"devDependencies": {
"@babel/core": "^7.19.1",
"@babel/node": "^7.19.1",
"@babel/preset-env": "^7.19.1",
"babel-cli": "^6.26.0",
"babel-core": "^6.26.3",
"babel-plugin-add-module-exports": "^1.0.4",
"babel-plugin-external-helpers": "^6.22.0",
"babel-plugin-transform-flow-strip-types": "^6.22.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.7.0",
"babel-preset-es2015": "^6.24.1",
"concurrently": "^4.1.1",
"coveralls": "^3.0.2",
"cross-env": "^5.2.0",
"documentation": "^9.1.1",
"eslint": "^8.23.1",
"flow-bin": "^0.79.1",
"http-server": "^0.12.1",
"jest": "^23.5.0",
"jest-html-reporter": "^2.5.0",
"jest-stare": "^1.16.0",
"jquery": "^3.4.1",
"nodemon": "^1.19.1",
"release-it": "^12.3.0",
"rimraf": "^2.6.2",
"rollup": "^0.65.0",
"rollup-plugin-babel": "^3.0.7",
"rollup-plugin-commonjs": "^9.1.6",
"rollup-plugin-filesize": "^4.0.1",
"rollup-plugin-hypothetical": "^2.1.0",
"rollup-plugin-ignore": "^1.0.4",
"rollup-plugin-json": "^3.0.0",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.2.1",
"rollup-plugin-node-resolve": "^3.3.0",
"rollup-plugin-terser": "^1.0.1"
},
"peerDependencies": {
"gun": "^0.2020.1238"
},
"dependencies": {
"fuse.js": "^6.6.2",
"htm": "^3.1.1",
"identicon.js": "^2.3.3",
"jsxstyle": "^2.5.1",
"preact": "^10.11.0",
"preact-custom-element": "^4.2.1"
}
}

64
iris-lib/rollup.config.js Normal file
View File

@ -0,0 +1,64 @@
import json from 'rollup-plugin-json';
import babel from 'rollup-plugin-babel';
import nodeResolve from 'rollup-plugin-node-resolve';
import {terser} from 'rollup-plugin-terser';
import filesize from 'rollup-plugin-filesize';
import commonjs from 'rollup-plugin-commonjs';
import builtins from 'rollup-plugin-node-builtins';
import globals from 'rollup-plugin-node-globals';
import hypothetical from 'rollup-plugin-hypothetical';
import ignore from 'rollup-plugin-ignore';
const name = `iris`;
const plugins = [
ignore(['gun/lib/then', 'gun/lib/load']),
hypothetical({
allowFallthrough: true,
files: {
'node-webcrypto-ossl/': `
export default {};
`,
'text-encoding/': `
export default {};
`,
'@trust/webcrypto/': `
export default {};
`
}
}),
json(),
babel({
exclude: 'node_modules/**',
plugins: ['external-helpers', 'transform-runtime'],
runtimeHelpers: true
}),
builtins(),
nodeResolve({
module: true,
jsnext: true,
browser: true,
}),
commonjs({
include: `node_modules/**`
}),
filesize(),
globals()
];
const isProd = process.env.NODE_ENV === `production`;
if (isProd) plugins.push(terser());
export default {
input: `src/index.js`,
external: ['gun', 'gun/sea'],
plugins,
output: {
file: `dist/${name}${isProd ? `.min` : ``}.js`,
name,
format: `umd`,
globals: {
gun: 'Gun'
}
}
};

View File

@ -558,7 +558,7 @@ class Channel {
if (o.from && o.from !== pub) { return; }
if (permissions.write) {
this._onTheirGroupFromUser(pub, o.key, o.callback);
} else { // unsubscribe
} else { // unsubscribe
o.event && o.event.off();
}
});
@ -731,7 +731,7 @@ class Channel {
});
}
async onTheirDirect(key, callback, from) { // TODO: subscribe to new channel participants
async onTheirDirect(key, callback, from) { // TODO: subscribe to new channel participants
if (typeof callback !== 'function') {
throw new Error(`onTheir callback must be a function, got ${typeof callback}`);
}
@ -954,7 +954,7 @@ class Channel {
const typingIndicator = util.createElement('div', 'iris-typing-indicator', chatBox);
typingIndicator.innerText = 'typing...';
this.getTyping(isTyping => {
typingIndicator.setAttribute('class', `iris-typing-indicator${ isTyping ? ' yes' : ''}`);
typingIndicator.setAttribute('class', `iris-typing-indicator${ isTyping ? ' yes' : ''}`);
});
const inputWrapper = util.createElement('div', 'iris-chat-input-wrapper', chatBox);
@ -978,13 +978,13 @@ class Channel {
const pub = participants[0];
this.gun.user(pub).get('profile').get('name').on(name => nameEl.innerText = name);
Channel.getActivity(this.gun, pub, status => {
const cls = `iris-online-indicator${ status.isActive ? ' yes' : ''}`;
const cls = `iris-online-indicator${ status.isActive ? ' yes' : ''}`;
onlineIndicator.setAttribute('class', cls);
const undelivered = messages.querySelectorAll('.iris-chat-message:not(.delivered)');
undelivered.forEach(msg => {
if (msg.getAttribute('data-time') <= status.lastActive) {
const c = msg.getAttribute('class');
msg.setAttribute('class', `${c } delivered`);
msg.setAttribute('class', `${c } delivered`);
}
});
});
@ -997,7 +997,7 @@ class Channel {
if (msgEl.getAttribute('data-time') <= time) {
const msgClass = msgEl.getAttribute('class');
if (msgClass.indexOf('delivered') === -1) {
msgEl.setAttribute('class', `${msgClass } delivered`);
msgEl.setAttribute('class', `${msgClass } delivered`);
}
indicator.setAttribute('class', 'iris-seen yes');
}
@ -1041,7 +1041,7 @@ class Channel {
const content = textArea.value;
const caret = util.getCaret(textArea);
if (event.shiftKey) {
textArea.value = `${content.substring(0, caret - 1) }\n${ content.substring(caret, content.length)}`;
textArea.value = `${content.substring(0, caret - 1) }\n${ content.substring(caret, content.length)}`;
} else {
textArea.value = content.substring(0, caret - 1) + content.substring(caret, content.length);
this.send(textArea.value);

View File

@ -101,7 +101,7 @@ export default {
let result = '';
for (let i = 0; i < raw.length; i++) {
const hex = raw.charCodeAt(i).toString(16);
result += (hex.length === 2 ? hex : `0${ hex}`);
result += (hex.length === 2 ? hex : `0${ hex}`);
}
return result;
},
@ -123,7 +123,7 @@ export default {
} else if (document.selection) {
el.focus();
const r = document.selection.createRange();
if (r == null) {
if (r === null) {
return 0;
}
const re = el.createTextRange(), rc = re.duplicate();
@ -528,7 +528,7 @@ export default {
const t = date.toLocaleTimeString(undefined, {timeStyle: 'short'});
const s = t.split(':');
if (s.length === 3) { // safari tries to display seconds
return `${s[0] }:${ s[1] }${s[2].slice(2)}`;
return `${s[0] }:${ s[1] }${s[2].slice(2)}`;
}
return t;
},
@ -537,7 +537,7 @@ export default {
const t = date.toLocaleString(undefined, {dateStyle: 'short', timeStyle: 'short'});
const s = t.split(':');
if (s.length === 3) { // safari tries to display seconds
return `${s[0] }:${ s[1] }${s[2].slice(2)}`;
return `${s[0] }:${ s[1] }${s[2].slice(2)}`;
}
return t;
},
@ -566,10 +566,10 @@ export default {
return 'today';
}
const dayDifference = Math.round((now - date) / (1000 * 60 * 60 * 24));
if(dayDifference == 0){
if (dayDifference === 0) {
return 'today';
}
if (dayDifference == 1) {
if (dayDifference === 1) {
return 'yesterday';
}
if (dayDifference <= 5) {

11261
iris-lib/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -6,10 +6,9 @@
"build": "preact build --no-prerender",
"serve": "sirv build --port 8080 --cors --single",
"dev": "preact watch --host localhost --sw",
"lint": "eslint src",
"lint": "eslint src;eslint iris-lib/src",
"test": "jest",
"build:lib": "echo 'placeholder for building lib'",
"build:docs": "documentation build src/js/iris-lib/** -f html -o docs"
"build:docs": "documentation build iris-lib/src/** -f html -o docs"
},
"eslintConfig": {
"extends": "preact",
@ -50,6 +49,7 @@
"webpack-build-notifier": "^2.3.0"
},
"dependencies": {
"iris-lib": "file:./iris-lib",
"@walletconnect/web3-provider": "^1.7.8",
"@zxing/library": "^0.18.6",
"aether-torrent": "^0.3.0",

View File

@ -1,7 +1,7 @@
import {translate as t} from './translations/Translation';
import $ from 'jquery';
import _ from 'lodash';
import iris from './iris-lib';
import iris from 'iris-lib';
import Autolinker from 'autolinker';
const emojiRegex = /[\u{1f300}-\u{1f5ff}\u{1f900}-\u{1f9ff}\u{1f600}-\u{1f64f}\u{1f680}-\u{1f6ff}\u{2600}-\u{26ff}\u{2700}-\u{27bf}\u{1f1e6}-\u{1f1ff}\u{1f191}-\u{1f251}\u{1f004}\u{1f0cf}\u{1f170}-\u{1f171}\u{1f17e}-\u{1f17f}\u{1f18e}\u{3030}\u{2b50}\u{2b55}\u{2934}-\u{2935}\u{2b05}-\u{2b07}\u{2b1b}-\u{2b1c}\u{3297}\u{3299}\u{303d}\u{00a9}\u{00ae}\u{2122}\u{23f3}\u{24c2}\u{23e9}-\u{23ef}\u{25b6}\u{23f8}-\u{23fa}]+/ug;

View File

@ -3,7 +3,7 @@ import Session from './Session';
import { route } from 'preact-router';
import State from './State';
import _ from 'lodash';
import iris from './iris-lib';
import iris from 'iris-lib';
import Gun from 'gun';
import $ from 'jquery';

View File

@ -4,7 +4,7 @@ import Notifications from './Notifications';
import Helpers from './Helpers';
import PeerManager from './PeerManager';
import { route } from 'preact-router';
import iris from './iris-lib';
import iris from 'iris-lib';
import _ from 'lodash';
import Fuse from "./lib/fuse.basic.esm.min";
import localforage from './lib/localforage.min';

View File

@ -9,7 +9,7 @@ import _ from 'lodash';
import Fun from './Fun';
import PeerManager from './PeerManager';
import iris from './iris-lib';
import iris from 'iris-lib';
import Helpers from './Helpers';
const State = {

View File

@ -2,7 +2,7 @@ import { Component } from 'preact';
import Helpers from '../Helpers';
import {translate as t} from '../translations/Translation';
import $ from 'jquery';
import iris from '../iris-lib';
import iris from 'iris-lib';
import { OptionalGetter } from '../types';
import Button from './basic/Button';

View File

@ -9,7 +9,7 @@ import SafeImg from './SafeImg';
import Torrent from './Torrent';
import $ from 'jquery';
import EmojiButton from '../lib/emoji-button';
import iris from '../iris-lib';
import iris from 'iris-lib';
import SearchBox from './SearchBox';
import MessageForm from './MessageForm';

View File

@ -8,7 +8,7 @@ import { route } from 'preact-router';
import Identicon from './Identicon';
import SearchBox from './SearchBox';
import Icons from '../Icons';
import iris from '../iris-lib';
import iris from 'iris-lib';
import {Link} from "preact-router/match";
import $ from 'jquery';

View File

@ -1,6 +1,6 @@
import Component from '../BaseComponent';
import State from '../State';
import iris from '../iris-lib';
import iris from 'iris-lib';
import Identicon from 'identicon.js';
import SafeImg from './SafeImg';
import styled from 'styled-components';

View File

@ -4,7 +4,7 @@ import { html } from 'htm/preact';
import Session from '../Session';
import Torrent from './Torrent';
import Autolinker from 'autolinker';
import iris from '../iris-lib';
import iris from 'iris-lib';
import $ from 'jquery';
import State from '../State';
import {route} from 'preact-router';

View File

@ -1,7 +1,7 @@
import { Component } from 'preact';
import State from '../State';
import Session from '../Session';
import iris from '../iris-lib';
import iris from 'iris-lib';
function twice(f) {
f();

View File

@ -10,7 +10,7 @@ import Session from '../Session';
import Torrent from './Torrent';
import Icons from '../Icons';
import Autolinker from 'autolinker';
import iris from '../iris-lib';
import iris from 'iris-lib';
import $ from 'jquery';
import {Helmet} from "react-helmet";
import Notifications from '../Notifications';

View File

@ -13,7 +13,7 @@ import View from './View';
import SearchBox from '../components/SearchBox';
import {SMS_VERIFIER_PUB} from '../SMS';
import $ from 'jquery';
import iris from '../iris-lib';
import iris from 'iris-lib';
import Button from '../components/basic/Button';
function deleteChat(uuid) {

View File

@ -5,7 +5,7 @@ import Button from '../components/basic/Button';
import {translate as t} from '../translations/Translation';
import Name from '../components/Name';
import View from './View';
import iris from '../iris-lib';
import iris from 'iris-lib';
import PublicMessage from "../components/PublicMessage";
import NotificationTools from "../Notifications";

View File

@ -16,7 +16,7 @@ import View from './View';
import { Link } from 'preact-router/match';
import $ from 'jquery';
import QRCode from '../lib/qrcode.min';
import iris from '../iris-lib';
import iris from 'iris-lib';
import {Helmet} from "react-helmet";
import {SMS_VERIFIER_PUB} from '../SMS';
import ProfilePhoto from '../components/ProfilePhoto';

View File

@ -4,7 +4,7 @@ import { translate as t } from '../../translations/Translation';
import Torrent from '../../components/Torrent';
import State from '../../State';
import Session from '../../Session';
import iris from '../../iris-lib';
import iris from 'iris-lib';
import _ from 'lodash';
import $ from 'jquery';
import EmojiButton from '../../lib/emoji-button';

View File

@ -7,7 +7,7 @@ import CopyButton from '../../components/CopyButton';
import { route } from 'preact-router';
import $ from 'jquery';
import QRCode from '../../lib/qrcode.min';
import iris from '../../iris-lib';
import iris from 'iris-lib';
import Component from '../../BaseComponent';
import {Helmet} from 'react-helmet';
import Button from '../../components/basic/Button';

View File

@ -12,7 +12,7 @@ import Notifications from '../../Notifications';
import NewChat from './newchat/NewChat';
import _ from 'lodash';
import $ from 'jquery';
import iris from '../../iris-lib';
import iris from 'iris-lib';
import {Helmet} from 'react-helmet';
import Component from '../../BaseComponent';

View File

@ -3,7 +3,7 @@ import Session from '../../../Session';
import $ from 'jquery';
import Component from '../../../BaseComponent';
import Button from '../../../components/basic/Button';
import iris from '../../../iris-lib';
import iris from 'iris-lib';
import { route } from 'preact-router';
import State from '../../../State';

View File

@ -7,7 +7,7 @@ import Button from '../../../components/basic/Button';
import $ from 'jquery';
import Component from '../../../BaseComponent';
import { route } from 'preact-router';
import iris from '../../../iris-lib';
import iris from 'iris-lib';
class MainView extends Component {
constructor() {

View File

@ -7652,7 +7652,7 @@ hsla-regex@^1.0.0:
resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38"
integrity sha512-7Wn5GMLuHBjZCb2bTmnDOycho0p/7UVaAeqXZGbHrBCl6Yd/xDhQJAXe6Ga9AXJH2I5zY1dEdYw2u1UptnSBJA==
htm@^3.1.0:
htm@^3.1.0, htm@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/htm/-/htm-3.1.1.tgz#49266582be0dc66ed2235d5ea892307cc0c24b78"
integrity sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==
@ -8062,6 +8062,16 @@ ipaddr.js@1.9.1:
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0"
integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==
"iris-lib@file:./iris-lib":
version "0.0.159"
dependencies:
fuse.js "^6.6.2"
htm "^3.1.1"
identicon.js "^2.3.3"
jsxstyle "^2.5.1"
preact "^10.11.0"
preact-custom-element "^4.2.1"
is-absolute-url@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6"
@ -12173,7 +12183,7 @@ preact@10.4.1:
resolved "https://registry.yarnpkg.com/preact/-/preact-10.4.1.tgz#9b3ba020547673a231c6cf16f0fbaef0e8863431"
integrity sha512-WKrRpCSwL2t3tpOOGhf2WfTpcmbpxaWtDbdJdKdjd0aEiTkvOmS4NBkG6kzlaAHI9AkQ3iVqbFWM3Ei7mZ4o1Q==
preact@^10.5.14:
preact@^10.11.0, preact@^10.5.14:
version "10.11.0"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.11.0.tgz#26af45a0613f4e17a197cc39d7a1ea23e09b2532"
integrity sha512-Fk6+vB2kb6mSJfDgODq0YDhMfl0HNtK5+Uc9QqECO4nlyPAQwCI+BKyWO//idA7ikV7o+0Fm6LQmNuQi1wXI1w==