Compare commits

..

10 Commits

29 changed files with 1151 additions and 319 deletions

View File

@ -1,4 +1,5 @@
.svn .svn
db.sqlite db.*
out/**/*.o out/**/*.o
out/**/*.d out/**/*.d
NOTES.md

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ out
*.swo *.swo
*.swp *.swp
.zsign_cache/ .zsign_cache/
NOTES.md

5
.markdownlint.yaml Normal file
View File

@ -0,0 +1,5 @@
default: true
MD010: false # Ignore tabs in code blocks
MD013: false # Don't wrap lines by default
MD046:
style: "fenced" # Force fenced code blocks

View File

@ -12,3 +12,8 @@ deps
apps/ssb/tribute.esm.js apps/ssb/tribute.esm.js
apps/api/app.js apps/api/app.js
**/emojis.json **/emojis.json
# only markdownlint should deal with the documentation
docs/**/*.md
NOTES.md

View File

@ -1,4 +1,4 @@
Copyright 2014 Cory McWilliams Copyright 2014-2024 Cory McWilliams
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in this software and associated documentation files (the "Software"), to deal in

View File

@ -4,46 +4,19 @@ Tilde Friends is a tool for making and sharing.
A public instance lives at https://www.tildefriends.net/. A public instance lives at https://www.tildefriends.net/.
It is both a peer-to-peer social network client, participating in Secure It is both a peer-to-peer social network client, participating in Secure Scuttlebutt, as well as a platform for writing and running web applications.
Scuttlebutt, as well as a platform for writing and running web applications.
## Goals ## Goals
1. Make it easy and fun to run all sorts of web applications. 1. Make it easy and fun to run all sorts of web applications.
2. Provide security that is easy to understand and protects your data. 2. Provide security that is easy to understand and protects your data.
3. Make creating and sharing web applications accessible to anyone with a 3. Make creating and sharing web applications accessible to anyone with a browser.
browser.
## Building
Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. Builds for
all of those host platforms plus mingw64, iOS, and android.
1. Requires openssl (`libssl-dev`, in debian-speak). All other dependencies
are kept up to date in the tree.
2. To build, run `make debug` or `make release`. An executable will be
generated in a subdirectory of `out/`.
3. It's possible to build for Android, iOS, and Windows on Linux, if you have
the right dependencies in the right places. `make windebug winrelease
iosdebug-ipa iosrelease-ipa release-apk`.
4. To build in docker, `docker build .`.
5. `make format` will normalize formatting to the coding standard.
## Running
By default, running the built `tildefriends` executable will start a web server
at <http://localhost:12345/>. `tildefriends -h` lists further options.
The first user to create an account and log in will be granted administrative
privileges. Further administration can be done at
<http://localhost:12345/~core/admin/>.
## Documentation ## Documentation
Docs are a work in progress: Docs are a work in progress in the `docs` folder, or alternatively in Tilde Friends: <https://www.tildefriends.net/~cory/wiki/#test-wiki/tf-app-quick-reference>.
<https://www.tildefriends.net/~cory/wiki/#test-wiki/tf-app-quick-reference>.
## License ## License
All code unless otherwise noted in is provided under the All code, documentation and assets unless otherwise noted in is provided under the
[MIT](https://opensource.org/licenses/MIT) license. [MIT](https://opensource.org/licenses/MIT/) license.

View File

@ -1,5 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "🐌", "emoji": "🐌",
"previous": "&raSj7ozmSDNGmB6TtjDk7oOiTc33ZN+RrBMASJ2F4cA=.sha256" "previous": "&vEaOZjrNb0u9rhNqrQ8eU9TlOFlo4HsgW6hbI7VdIT0=.sha256"
} }

View File

@ -264,7 +264,6 @@ class TfElement extends LitElement {
hash=${this.hash} hash=${this.hash}
.unread=${this.unread} .unread=${this.unread}
@refresh=${() => (this.unread = [])} @refresh=${() => (this.unread = [])}
?loading=${this.loading}
></tf-tab-news> ></tf-tab-news>
`; `;
} else if (this.tab === 'connections') { } else if (this.tab === 'connections') {

View File

@ -7,11 +7,9 @@ class TfTabConnectionsElement extends LitElement {
return { return {
broadcasts: {type: Array}, broadcasts: {type: Array},
identities: {type: Array}, identities: {type: Array},
my_identities: {type: Array},
connections: {type: Array}, connections: {type: Array},
stored_connections: {type: Array}, stored_connections: {type: Array},
users: {type: Object}, users: {type: Object},
server_identity: {type: String},
}; };
} }
@ -22,22 +20,15 @@ class TfTabConnectionsElement extends LitElement {
let self = this; let self = this;
this.broadcasts = []; this.broadcasts = [];
this.identities = []; this.identities = [];
this.my_identities = [];
this.connections = []; this.connections = [];
this.stored_connections = []; this.stored_connections = [];
this.users = {}; this.users = {};
tfrpc.rpc.getIdentities().then(function (identities) {
self.my_identities = identities || [];
});
tfrpc.rpc.getAllIdentities().then(function (identities) { tfrpc.rpc.getAllIdentities().then(function (identities) {
self.identities = identities || []; self.identities = identities || [];
}); });
tfrpc.rpc.getStoredConnections().then(function (connections) { tfrpc.rpc.getStoredConnections().then(function (connections) {
self.stored_connections = connections || []; self.stored_connections = connections || [];
}); });
tfrpc.rpc.getServerIdentity().then(function (identity) {
self.server_identity = identity;
});
} }
render_connection_summary(connection) { render_connection_summary(connection) {
@ -187,12 +178,6 @@ class TfTabConnectionsElement extends LitElement {
${this.identities.map( ${this.identities.map(
(x) => (x) =>
html`<li class="w3-bar"> html`<li class="w3-bar">
${x == this.server_identity ?
html`<span class="w3-tag w3-medium w3-round w3-theme-l1">🖥 local server</span>` :
undefined}
${this.my_identities.indexOf(x) != -1 ?
html`<span class="w3-tag w3-medium w3-round w3-theme-d1">😎 you</span>` :
undefined}
<tf-user id=${x} .users=${this.users}></tf-user> <tf-user id=${x} .users=${this.users}></tf-user>
</li>` </li>`
)} )}

View File

@ -12,7 +12,6 @@ class TfTabNewsElement extends LitElement {
following: {type: Array}, following: {type: Array},
drafts: {type: Object}, drafts: {type: Object},
expanded: {type: Object}, expanded: {type: Object},
loading: {type: Boolean},
}; };
} }
@ -114,15 +113,6 @@ class TfTabNewsElement extends LitElement {
.users=${this.users} .users=${this.users}
></tf-profile>` ></tf-profile>`
: undefined; : undefined;
let edit_profile;
if (!this.loading &&
this.users[this.whoami]?.name === undefined &&
this.hash.substring(1) != this.whoami) {
edit_profile = html`
<div class="w3-panel w3-padding w3-round w3-card-4 w3-theme-l3">
Follow your identity link ☝️ above to edit your profile and set your name.
</div>`;
}
return html` return html`
<p class="w3-bar"> <p class="w3-bar">
<button <button
@ -134,7 +124,6 @@ class TfTabNewsElement extends LitElement {
</p> </p>
<div> <div>
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>! Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
${edit_profile}
</div> </div>
<div> <div>
<tf-compose <tf-compose

View File

@ -1,4 +1,6 @@
# Philosophy # Tilde Friends
## Philosophy
Tilde Friends is a platform for making, running, and sharing web applications. Tilde Friends is a platform for making, running, and sharing web applications.
@ -18,7 +20,7 @@ Above the terminal, an "Edit" link brings a visitor to the source code for the
current Tilde Friends application, which they can then edit, save as their own, current Tilde Friends application, which they can then edit, save as their own,
and run. and run.
# Architecture ## Architecture
Tilde Friends is a C++ application with a JavaScript runtime that provides Tilde Friends is a C++ application with a JavaScript runtime that provides
restricted access to filesystem, network, and other system resources. The core restricted access to filesystem, network, and other system resources. The core
@ -66,7 +68,7 @@ performance reasons to minimize the data size transferred between processes.
// Receive the above message and call the function. // Receive the above message and call the function.
core.register("onMessage", function(sender, message) { core.register("onMessage", function(sender, message) {
message.add(3, 4).then(x => terminal.print(x.toString())); message.add(3, 4).then(x => terminal.print(x.toString()));
}); });
Finally, there is a core web interface that runs on the client's browser that Finally, there is a core web interface that runs on the client's browser that
@ -135,16 +137,18 @@ Sets the browser window/tab title.
Reconfigures the terminal layout, potentially into multiple split panes. Reconfigures the terminal layout, potentially into multiple split panes.
terminal.split([ ```javascript
{ terminal.split(
type: "horizontal", [{
children: [ type: "horizontal",
{name: "left", basis: "2in", grow: 0, shrink: 0}, children: [
{name: "middle", grow: 1}, {name: "left", basis: "2in", grow: 0, shrink: 0},
{name: "right", basis: "2in", grow: 0, shrink: 0}, {name: "middle", grow: 1},
], {name: "right", basis: "2in", grow: 0, shrink: 0},
}, ],
]); }]
);
```
#### terminal.select(name) #### terminal.select(name)

50
docs/apps/quickstart.md Normal file
View File

@ -0,0 +1,50 @@
# Writing Tilde Friends applications7
TODO
## Creating your environment
1. Open an existing application (ie: `identity`);
2. Open the editing panel;
3. Save the app under a new name (ie `/~YOUR_USERNAME/my-app/`);
4. Go back to the main menu and open your new app;
5. You can now edit your app, save it and see changes in the real time.
## Project structure
An application has a `app.js` file that gets run when a user enters the app.
This file contains a function (typically called `main()`) that's considered the entry point.
Paste this in `app.js`:
```javascript
async function main() {
let ids = await ssb.getIdentities();
await app.setDocument(`
<body style="font-family: sans-serif; color: white">
<h1>Hello world!</h1>
</body>
</body>`);
}
main();
```
Save the app, and you should now be seeing `Hello world!` on the screen.
## Components
Once your app grows to a certain size, you'll want to introduce components.
In Tilde Friends, the de facto standard is [Lit](TODO).
Althogh you an use any framework you want, you're encouraged to use Lit as you can reuse
First, add lit-all-min.js into your project.
TODO
<!-- mention shadow dom -->
TODO: tfrpc
TODO: sharing apps

76
docs/building.md Normal file
View File

@ -0,0 +1,76 @@
# How to build Tilde Friends
> Disclaimer: this documentation has been written by a Linux user and has not been reviewed by other people on other platforms. The procedure may vary slightly depending on your operating system.
Builds **on** Linux (`x86_64` and `aarch64`), MacOS, OpenBSD, and Haiku.
Builds **for** all of those host platforms plus `mingw64`, iOS, and android.
Dependencies:
- `openssl` (`libssl-dev`, in debian-speak)
Dependencies for Android:
- TODO
Dependencies for iOS:
- TODO
Dependencies for Windows:
- TODO
> All other dependencies are kept up to date as git submodules.
1. Clone the repository with the submodules: `git clone --recursive https://dev.tildefriends.net/cory/tildefriends.git`
2. Run `make -j $(nproc) debug` or `make -j $(nproc) release`
If you're unsure whether you should choose `debug` or `release`, stick to `release`.
> `-j $(nproc)` will start a compiler for every CPU thread, which will dramatically reduce the time needed to compile Tilde Friends.
An executable will be generated in a subdirectory of `out/`
It's possible to build for Android, iOS, and Windows on Linux, if you have the right dependencies in the right places. Run `make -j $(nproc) windebug winrelease iosdebug-ipa iosrelease-ipa release-apk`
To build in docker, `docker build .`
<!-- On NixOS: TODO -->
<!-- Add shell.nix and nix derivs first -->
Now that you have a binary, head over to <running.md>.
## Troubleshooting
### The compiler throws an error and I can't build the binary
Open `GNUMakefile` and edit the CFLAGS environment variable around line 50.
For example given this error:
```text
src/http.c: In function 'tf_http_get_cookie':
src/http.c:1089:128: error: check of 'name' for NULL after already dereferencing it [-Werror=analyzer-deref-before-check]
```
Add:
```diff
CFLAGS += \
-std=gnu11 \
-Wall \
-Wextra \
-Wno-unused-parameter \
+ -Wno-analyzer-deref-before-check \
-MMD \
-MP \
-ffunction-sections \
-fdata-sections \
-fno-exceptions \
-g
```
Now the compiler will ignore this error and *should* continue building anyways.

42
docs/contributing.md Normal file
View File

@ -0,0 +1,42 @@
# How to contribute
- Fork this repository
- Clone your repository
Alternatively, you can change the `origin` remote on your existing clone:
`git remote set-url origin https://dev.tildefriends.net/YOUR_USERNAME/tildefriends.git`
- Make your changes
- I want to edit C code !
TODO
- I want to edit JavaScript code !
TODO
- I want to write documentation !
Great! Before you do, have a look at the [documentation guidelines](guidelines/documentation-guidelines.md) to learn how to write consistent documentation.
In all cases:
- Make sure that your commit messages are descriptive.
<!-- - hi -->
- Format your changes:
If you've edited C code: run `make format`
If you've edited JavaScript code or the documentation: run `npm run format`
- Open a pull request
TODO
- Get your changes reviewed and merged
TODO

19
docs/documentation.md Normal file
View File

@ -0,0 +1,19 @@
# Tilde Friends documentation
## Building
See <building.md>.
## Contibuting
See <contributing.md>.
## FAQ / Troubleshooting
See <faq.md>.
## Guide
This document will be phased out and integrated into the new documentation.
See <.guide.md>.

9
docs/faq.md Normal file
View File

@ -0,0 +1,9 @@
# Troubleshooting
## I started tildefriends. Now what ?
See <running.md>.
### The compiler throws an error and I can't build the binary
See <building.md>.

View File

@ -0,0 +1 @@
TODO

View File

@ -0,0 +1,64 @@
# Documentation guidelines
This document defines the rules used to write documentation in order to make it more consistent.
This documentation is a living document and so are it's rules; you are free to propose changes but in the meantime, please stick to them.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119/).
## File naming
Files SHOULD be named using [kebab-case](https://www.freecodecamp.org/news/snake-case-vs-camel-case-vs-pascal-case-vs-kebab-case-whats-the-difference/#kebab-case).
Their names should be meaningful and SHOULD not conflict with other files in other directories:
> Example: this document is named `docs/guidelines/documentation-guidelines.md` instead of `docs/guidelines/documentation.md` because it could cause confusion with `docs/documentation.md`.
## Documentation
When writing documentation, the author should have in mind it's target audience: people with varying technical skills and backgrounds, fluency in peer-to-peer-specific terms and mental ability.
The documentation should therefore be acessible and usefule to most people interested in building, using and contributing to Tilde Friends.
### Terminology
`Tilde Friends` refers to the projectas a whole. This can be abbreviated to `TF`.
`tildefriends` refers to the program.
### Style guide
1. Lines SHOULD NOT be wrapped, to allow clients to dynamically wrap them however they want:
```text
This is not very pleasant to read because
the text
is manually wrapped, but the size of the
screen is
smaller than the size the text is wrapped
at. I
need to write even more useless text here
so I get
my point across. Also hi! If you're here
that
means you're either going to contribute to
Tilde
Friends, or that you're reviewing my
stupid
changes. Either way, you're awesome!
```
You MAY use one line per sentence.
2. Lines ending with an `inline code block` SHOULD NOT end with a period.
> Example: To build in docker, `docker build .`
NB: this does not apply to file names or other text that are not meant to be copy-pasted.
> Example: this document is named `docs/guidelines/documentation-guidelines.md` instead of `docs/guidelines/documentation.md` because it could cause confusion with `docs/documentation.md`.
More TODO
## License
As per the rest of the code in this repository, the documentation is shared under the [MIT](https://opensource.org/licenses/MIT/) license.

View File

@ -0,0 +1 @@
TODO

50
docs/running.md Normal file
View File

@ -0,0 +1,50 @@
# Running Tilde Friends
> Disclaimer: this documentation has been written by a Linux user and has not been reviewed by other people on other platforms. The procedure may vary slightly depending on your operating system.
The binaries should appear at `out/debug/tildefriends` and `out/release/tildefriends`.
For Android, iOS and Windows: TODO
You can now start the server by running `./out/debug/tildefriends` or `./out/release/tildefriends`.
By default, running the built `tildefriends` executable will start a web server
at <http://localhost:12345/>. `tildefriends -h` lists further options.
## How to use TF
### Initial setup
Now you have a Tilde Friends instance running. The first thing you'll want to do is create your account. Click "login" in the top right corner, then "Register".
Enter your username and password.
> The first user to create an account and log in will be granted administrative privileges.
> Further administration can be done at <http://localhost:12345/~core/admin/>
Next, create a Scuttlebutt identity by pressing the "Create an identity" button.
This will create a pair of keys that are used to sign your messages with.
Because of the way Scuttlebutt is designed, you cannot log into your account without your keys.
Tilde Friends locks your keys behind a password, but if you were to destroy your database, the keys would be gone forever, and with it your possibility to send messages using this account. Click on the `identity` app and under "Identities", export your newly created identity.
You'll be prompted with a dialog box saying "This app is requesting the following permission:ssb_id_export".
This is because applications are not trusted to have access to your keys by default.
Click on "Allow" and you'll see a list of 12 words. You need to write those down in a password manager or on a piece of paperand keep it private and secure.
> Warning: Nobody needs to know these 12 words. Anybody that has access to those keys can post messages as you, see your private messages and documents and much more.
Now that your keys are safe, we can start connecting to the outside world.
### Replication
You've probably noticed asdtring of random characters by now. This is your public key, a unique identifier for your account you can share to anyone. If you go back to the home menu and into the `ssb` app, you can click on your public key. This will lead you to your profile, which is empty at the time. Edit it and enter your name.
TODO: joining a room
TODO: initial sync
TODO: send messages
TODO: how messages spread to friends
TODO: other apps

716
package-lock.json generated
View File

@ -6,13 +6,442 @@
"": { "": {
"name": "tildefriends", "name": "tildefriends",
"license": "MIT", "license": "MIT",
"devDependencies": {
"markdownlint-cli": "0.40.0",
"prettier": "3.2.5"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"dependencies": { "dependencies": {
"prettier": "^3.2.5" "string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/commander": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz",
"integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==",
"dev": true,
"engines": {
"node": ">=18"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dev": true,
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"dev": true,
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true,
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/foreground-child": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
"integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.0",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/get-stdin": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz",
"integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/glob": {
"version": "10.3.14",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.14.tgz",
"integrity": "sha512-4fkAqu93xe9Mk7le9v0y3VrPDqLKHarNi2s4Pv7f2yOvfhWfhc7hRPHC/JyqMqb8B/Dt/eGS4n7ykwf3fOsl8g==",
"dev": true,
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^2.3.6",
"minimatch": "^9.0.1",
"minipass": "^7.0.4",
"path-scurry": "^1.11.0"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/ignore": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
"integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
"dev": true,
"engines": {
"node": ">= 4"
}
},
"node_modules/ini": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/ini/-/ini-4.1.2.tgz",
"integrity": "sha512-AMB1mvwR1pyBFY/nSevUX6y8nJWS63/SzUKD3JyQn97s4xgIdgQPT75IRouIiBAN4yLQBUShNYVW0+UG25daCw==",
"dev": true,
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
"node_modules/jackspeak": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
"integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
"dev": true,
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/jsonc-parser": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz",
"integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==",
"dev": true
},
"node_modules/jsonpointer": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz",
"integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"dev": true,
"dependencies": {
"uc.micro": "^2.0.0"
}
},
"node_modules/lru-cache": {
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz",
"integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==",
"dev": true,
"engines": {
"node": "14 || >=16.14"
}
},
"node_modules/markdown-it": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"dev": true,
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.4.0",
"linkify-it": "^5.0.0",
"mdurl": "^2.0.0",
"punycode.js": "^2.3.1",
"uc.micro": "^2.1.0"
},
"bin": {
"markdown-it": "bin/markdown-it.mjs"
}
},
"node_modules/markdownlint": {
"version": "0.34.0",
"resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.34.0.tgz",
"integrity": "sha512-qwGyuyKwjkEMOJ10XN6OTKNOVYvOIi35RNvDLNxTof5s8UmyGHlCdpngRHoRGNvQVGuxO3BJ7uNSgdeX166WXw==",
"dev": true,
"dependencies": {
"markdown-it": "14.1.0",
"markdownlint-micromark": "0.1.9"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/DavidAnson"
}
},
"node_modules/markdownlint-cli": {
"version": "0.40.0",
"resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.40.0.tgz",
"integrity": "sha512-JXhI3dRQcaqwiFYpPz6VJ7aKYheD53GmTz9y4D/d0F1MbZDGOp9pqKlbOfUX/pHP/iAoeiE4wYRmk8/kjLakxA==",
"dev": true,
"dependencies": {
"commander": "~12.0.0",
"get-stdin": "~9.0.0",
"glob": "~10.3.12",
"ignore": "~5.3.1",
"js-yaml": "^4.1.0",
"jsonc-parser": "~3.2.1",
"jsonpointer": "5.0.1",
"markdownlint": "~0.34.0",
"minimatch": "~9.0.4",
"run-con": "~1.3.2",
"toml": "~3.0.0"
},
"bin": {
"markdownlint": "markdownlint.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/markdownlint-micromark": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/markdownlint-micromark/-/markdownlint-micromark-0.1.9.tgz",
"integrity": "sha512-5hVs/DzAFa8XqYosbEAEg6ok6MF2smDj89ztn9pKkCtdKHVdPQuGMH7frFfYL9mLkvfFe4pTyAMffLbjf3/EyA==",
"dev": true,
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/DavidAnson"
}
},
"node_modules/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
"dev": true
},
"node_modules/minimatch": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minipass": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.1.tgz",
"integrity": "sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA==",
"dev": true,
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/path-scurry": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.0.tgz",
"integrity": "sha512-LNHTaVkzaYaLGlO+0u3rQTz7QrHTFOuKyba9JMTQutkmtNew8dw8wOD7mTU/5fCPZzCWpfW0XnQKzY61P0aTaw==",
"dev": true,
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.2.5", "version": "3.2.5",
"license": "MIT", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
"dev": true,
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
}, },
@ -22,6 +451,289 @@
"funding": { "funding": {
"url": "https://github.com/prettier/prettier?sponsor=1" "url": "https://github.com/prettier/prettier?sponsor=1"
} }
},
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/run-con": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/run-con/-/run-con-1.3.2.tgz",
"integrity": "sha512-CcfE+mYiTcKEzg0IqS08+efdnH0oJ3zV0wSUFBNrMHMuxCtXvBCLzCJHatwuXDcu/RlhjTziTo/a1ruQik6/Yg==",
"dev": true,
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~4.1.0",
"minimist": "^1.2.8",
"strip-json-comments": "~3.1.1"
},
"bin": {
"run-con": "cli.js"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true,
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/toml": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==",
"dev": true
},
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"dev": true
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
} }
} }
} }

View File

@ -1,11 +1,14 @@
{ {
"name": "tildefriends", "name": "tildefriends",
"scripts": { "scripts": {
"prettier": "prettier . --check --cache --write" "format": "npm run prettier && npm run markdown",
"prettier": "npx prettier --cache --write --check .",
"markdown": "npx markdownlint-cli --fix 'docs/**/*.md'"
}, },
"author": "Cory McWilliams", "author": "Cory McWilliams",
"license": "MIT", "license": "MIT",
"dependencies": { "devDependencies": {
"prettier": "^3.2.5" "markdownlint-cli": "0.40.0",
"prettier": "3.2.5"
} }
} }

View File

@ -31,10 +31,6 @@
#define tf_countof(a) ((int)(sizeof((a)) / sizeof(*(a)))) #define tf_countof(a) ((int)(sizeof((a)) / sizeof(*(a))))
#define CYAN "\e[1;36m"
#define MAGENTA "\e[1;35m"
#define RESET "\e[0m"
const int64_t k_refresh_interval = 1ULL * 7 * 24 * 60 * 60 * 1000; const int64_t k_refresh_interval = 1ULL * 7 * 24 * 60 * 60 * 1000;
static JSValue _authenticate_jwt(JSContext* context, const char* jwt); static JSValue _authenticate_jwt(JSContext* context, const char* jwt);
@ -420,7 +416,6 @@ static JSValue _httpd_endpoint_start(JSContext* context, JSValueConst this_val,
*listener = (httpd_listener_t) { .context = context, .tls = JS_DupValue(context, argv[1]) }; *listener = (httpd_listener_t) { .context = context, .tls = JS_DupValue(context, argv[1]) };
tf_tls_context_t* tls = tf_tls_context_get(listener->tls); tf_tls_context_t* tls = tf_tls_context_get(listener->tls);
int assigned_port = tf_http_listen(http, port, tls, _httpd_listener_cleanup, listener); int assigned_port = tf_http_listen(http, port, tls, _httpd_listener_cleanup, listener);
tf_printf(CYAN "~😎 Tilde Friends" RESET " is now up at " MAGENTA "http%s://127.0.0.1:%d/" RESET ".\n", tls ? "s" : "", assigned_port);
return JS_NewInt32(context, assigned_port); return JS_NewInt32(context, assigned_port);
} }
@ -1080,7 +1075,7 @@ static JSValue _authenticate_jwt(JSContext* context, const char* jwt)
tf_task_t* task = tf_task_get(context); tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task); tf_ssb_t* ssb = tf_task_get_ssb(task);
char public_key_b64[k_id_base64_len] = { 0 }; char public_key_b64[k_id_base64_len] = { 0 };
tf_ssb_db_identity_visit(ssb, ":admin", _public_key_visit, public_key_b64); tf_ssb_db_identity_visit(ssb, ":auth", _public_key_visit, public_key_b64);
const char* payload = jwt + dot[0] + 1; const char* payload = jwt + dot[0] + 1;
size_t payload_length = dot[1] - dot[0] - 1; size_t payload_length = dot[1] - dot[0] - 1;
@ -1150,12 +1145,15 @@ static void _visit_auth_identity(const char* identity, void* user_data)
static bool _get_auth_private_key(tf_ssb_t* ssb, uint8_t* out_private_key) static bool _get_auth_private_key(tf_ssb_t* ssb, uint8_t* out_private_key)
{ {
char id[k_id_base64_len] = { 0 }; char id[k_id_base64_len] = { 0 };
tf_ssb_db_identity_visit(ssb, ":admin", _visit_auth_identity, id); tf_ssb_db_identity_visit(ssb, ":auth", _visit_auth_identity, id);
if (*id) if (*id)
{ {
return tf_ssb_db_identity_get_private_key(ssb, ":admin", id, out_private_key, crypto_sign_SECRETKEYBYTES); return tf_ssb_db_identity_get_private_key(ssb, ":auth", id, out_private_key, crypto_sign_SECRETKEYBYTES);
}
else
{
return tf_ssb_db_identity_create(ssb, ":auth", out_private_key + crypto_sign_PUBLICKEYBYTES, out_private_key);
} }
return false;
} }
static const char* _make_session_jwt(tf_ssb_t* ssb, const char* name) static const char* _make_session_jwt(tf_ssb_t* ssb, const char* name)
@ -1164,15 +1162,21 @@ static const char* _make_session_jwt(tf_ssb_t* ssb, const char* name)
{ {
return NULL; return NULL;
} }
uint8_t private_key[crypto_sign_SECRETKEYBYTES] = { 0 };
if (!_get_auth_private_key(ssb, private_key))
{
return NULL;
}
uv_timespec64_t now = { 0 }; uv_timespec64_t now = { 0 };
uv_clock_gettime(UV_CLOCK_REALTIME, &now); uv_clock_gettime(UV_CLOCK_REALTIME, &now);
JSContext* context = tf_ssb_get_context(ssb);
const char* header_json = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}"; const char* header_json = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
char header_base64[256]; char header_base64[256];
sodium_bin2base64(header_base64, sizeof(header_base64), (uint8_t*)header_json, strlen(header_json), sodium_base64_VARIANT_URLSAFE_NO_PADDING); sodium_bin2base64(header_base64, sizeof(header_base64), (uint8_t*)header_json, strlen(header_json), sodium_base64_VARIANT_URLSAFE_NO_PADDING);
JSContext* context = tf_ssb_get_context(ssb);
JSValue payload = JS_NewObject(context); JSValue payload = JS_NewObject(context);
JS_SetPropertyStr(context, payload, "name", JS_NewString(context, name)); JS_SetPropertyStr(context, payload, "name", JS_NewString(context, name));
JS_SetPropertyStr(context, payload, "exp", JS_NewInt64(context, now.tv_sec * 1000 + now.tv_nsec / 1000000LL + k_refresh_interval)); JS_SetPropertyStr(context, payload, "exp", JS_NewInt64(context, now.tv_sec * 1000 + now.tv_nsec / 1000000LL + k_refresh_interval));
@ -1187,17 +1191,12 @@ static const char* _make_session_jwt(tf_ssb_t* ssb, const char* name)
unsigned long long signature_length = 0; unsigned long long signature_length = 0;
char signature_base64[256] = { 0 }; char signature_base64[256] = { 0 };
uint8_t private_key[crypto_sign_SECRETKEYBYTES] = { 0 }; if (crypto_sign_detached(signature, &signature_length, (const uint8_t*)payload_base64, strlen(payload_base64), private_key) == 0)
if (_get_auth_private_key(ssb, private_key))
{ {
if (crypto_sign_detached(signature, &signature_length, (const uint8_t*)payload_base64, strlen(payload_base64), private_key) == 0) sodium_bin2base64(signature_base64, sizeof(signature_base64), signature, sizeof(signature), sodium_base64_VARIANT_URLSAFE_NO_PADDING);
{ size_t size = strlen(header_base64) + 1 + strlen(payload_base64) + 1 + strlen(signature_base64) + 1;
sodium_bin2base64(signature_base64, sizeof(signature_base64), signature, sizeof(signature), sodium_base64_VARIANT_URLSAFE_NO_PADDING); result = tf_malloc(size);
size_t size = strlen(header_base64) + 1 + strlen(payload_base64) + 1 + strlen(signature_base64) + 1; snprintf(result, size, "%s.%s.%s", header_base64, payload_base64, signature_base64);
result = tf_malloc(size);
snprintf(result, size, "%s.%s.%s", header_base64, payload_base64, signature_base64);
}
sodium_memzero(private_key, sizeof(private_key));
} }
JS_FreeCString(context, payload_string); JS_FreeCString(context, payload_string);

View File

@ -48,7 +48,6 @@ static int _tf_command_import(const char* file, int argc, char* argv[]);
static int _tf_command_export(const char* file, int argc, char* argv[]); static int _tf_command_export(const char* file, int argc, char* argv[]);
static int _tf_command_run(const char* file, int argc, char* argv[]); static int _tf_command_run(const char* file, int argc, char* argv[]);
static int _tf_command_sandbox(const char* file, int argc, char* argv[]); static int _tf_command_sandbox(const char* file, int argc, char* argv[]);
static int _tf_command_verify(const char* file, int argc, char* argv[]);
static int _tf_command_usage(const char* file); static int _tf_command_usage(const char* file);
typedef struct _command_t typedef struct _command_t
@ -63,7 +62,6 @@ const command_t k_commands[] = {
{ "sandbox", _tf_command_sandbox, "Run a sandboxed tildefriends sandbox process (used internally)." }, { "sandbox", _tf_command_sandbox, "Run a sandboxed tildefriends sandbox process (used internally)." },
{ "import", _tf_command_import, "Import apps to SSB." }, { "import", _tf_command_import, "Import apps to SSB." },
{ "export", _tf_command_export, "Export apps from SSB." }, { "export", _tf_command_export, "Export apps from SSB." },
{ "verify", _tf_command_verify, "Verify a feed." },
{ "test", _tf_command_test, "Test SSB." }, { "test", _tf_command_test, "Test SSB." },
}; };
@ -268,59 +266,6 @@ static int _tf_command_export(const char* file, int argc, char* argv[])
tf_ssb_destroy(ssb); tf_ssb_destroy(ssb);
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }
static int _tf_command_verify(const char* file, int argc, char* argv[])
{
const char* identity = NULL;
const char* db_path = k_db_path_default;
bool show_usage = false;
while (!show_usage)
{
static const struct option k_options[] = {
{ "id", required_argument, NULL, 'u' },
{ "db-path", required_argument, NULL, 'd' },
{ "help", no_argument, NULL, 'h' },
{ 0 },
};
int c = getopt_long(argc, argv, "i:d:h", k_options, NULL);
if (c == -1)
{
break;
}
switch (c)
{
case '?':
case 'h':
default:
show_usage = true;
break;
case 'i':
identity = optarg;
break;
case 'd':
db_path = optarg;
break;
}
}
if (show_usage)
{
tf_printf("\n%s import [options] [paths...]\n\n", file);
tf_printf("options:\n");
tf_printf(" -i, --identity identity Identity to verify.\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", k_db_path_default);
tf_printf(" -h, --help Show this usage information.\n");
return EXIT_FAILURE;
}
tf_printf("Verifying %s...\n", identity);
tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
bool verified = tf_ssb_db_verify(ssb, identity);
tf_ssb_destroy(ssb);
return verified ? EXIT_SUCCESS : EXIT_FAILURE;
}
#endif #endif
typedef struct tf_run_args_t typedef struct tf_run_args_t

View File

@ -1019,18 +1019,7 @@ static bool _tf_ssb_verify_and_strip_signature_internal(JSContext* context, JSVa
bool tf_ssb_verify_and_strip_signature(JSContext* context, JSValue val, char* out_id, size_t out_id_size, char* out_signature, size_t out_signature_size, int* out_flags) bool tf_ssb_verify_and_strip_signature(JSContext* context, JSValue val, char* out_id, size_t out_id_size, char* out_signature, size_t out_signature_size, int* out_flags)
{ {
JSValue reordered = JS_NewObject(context); if (_tf_ssb_verify_and_strip_signature_internal(context, val, out_id, out_id_size, out_signature, out_signature_size))
JS_SetPropertyStr(context, reordered, "previous", JS_GetPropertyStr(context, val, "previous"));
JS_SetPropertyStr(context, reordered, "author", JS_GetPropertyStr(context, val, "author"));
JS_SetPropertyStr(context, reordered, "sequence", JS_GetPropertyStr(context, val, "sequence"));
JS_SetPropertyStr(context, reordered, "timestamp", JS_GetPropertyStr(context, val, "timestamp"));
JS_SetPropertyStr(context, reordered, "hash", JS_GetPropertyStr(context, val, "hash"));
JS_SetPropertyStr(context, reordered, "content", JS_GetPropertyStr(context, val, "content"));
JS_SetPropertyStr(context, reordered, "signature", JS_GetPropertyStr(context, val, "signature"));
bool result = _tf_ssb_verify_and_strip_signature_internal(context, reordered, out_id, out_id_size, out_signature, out_signature_size);
JS_FreeValue(context, reordered);
if (result)
{ {
if (out_flags) if (out_flags)
{ {
@ -1038,26 +1027,27 @@ bool tf_ssb_verify_and_strip_signature(JSContext* context, JSValue val, char* ou
} }
return true; return true;
} }
else
reordered = JS_NewObject(context);
JS_SetPropertyStr(context, reordered, "previous", JS_GetPropertyStr(context, val, "previous"));
JS_SetPropertyStr(context, reordered, "sequence", JS_GetPropertyStr(context, val, "sequence"));
JS_SetPropertyStr(context, reordered, "author", JS_GetPropertyStr(context, val, "author"));
JS_SetPropertyStr(context, reordered, "timestamp", JS_GetPropertyStr(context, val, "timestamp"));
JS_SetPropertyStr(context, reordered, "hash", JS_GetPropertyStr(context, val, "hash"));
JS_SetPropertyStr(context, reordered, "content", JS_GetPropertyStr(context, val, "content"));
JS_SetPropertyStr(context, reordered, "signature", JS_GetPropertyStr(context, val, "signature"));
result = _tf_ssb_verify_and_strip_signature_internal(context, reordered, out_id, out_id_size, out_signature, out_signature_size);
JS_FreeValue(context, reordered);
if (result)
{ {
if (out_flags) JSValue reordered = JS_NewObject(context);
JS_SetPropertyStr(context, reordered, "previous", JS_GetPropertyStr(context, val, "previous"));
JS_SetPropertyStr(context, reordered, "sequence", JS_GetPropertyStr(context, val, "sequence"));
JS_SetPropertyStr(context, reordered, "author", JS_GetPropertyStr(context, val, "author"));
JS_SetPropertyStr(context, reordered, "timestamp", JS_GetPropertyStr(context, val, "timestamp"));
JS_SetPropertyStr(context, reordered, "hash", JS_GetPropertyStr(context, val, "hash"));
JS_SetPropertyStr(context, reordered, "content", JS_GetPropertyStr(context, val, "content"));
JS_SetPropertyStr(context, reordered, "signature", JS_GetPropertyStr(context, val, "signature"));
bool result = _tf_ssb_verify_and_strip_signature_internal(context, reordered, out_id, out_id_size, out_signature, out_signature_size);
JS_FreeValue(context, reordered);
if (result)
{ {
*out_flags = k_tf_ssb_message_flag_sequence_before_author; if (out_flags)
{
*out_flags = k_tf_ssb_message_flag_sequence_before_author;
}
return true;
} }
return true;
} }
return false; return false;
} }
@ -3618,6 +3608,7 @@ void tf_ssb_verify_strip_and_store_message(tf_ssb_t* ssb, JSValue value, tf_ssb_
} }
else else
{ {
printf("nope\n");
_tf_ssb_verify_strip_and_store_finish(async); _tf_ssb_verify_strip_and_store_finish(async);
} }
} }

View File

@ -163,7 +163,6 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
" private_key TEXT UNIQUE" " private_key TEXT UNIQUE"
")"); ")");
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS identities_user ON identities (user, public_key)"); _tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS identities_user ON identities (user, public_key)");
_tf_ssb_db_exec(db, "DELETE FROM identities WHERE user = ':auth'");
bool populate_fts = false; bool populate_fts = false;
if (!_tf_ssb_db_has_rows(db, "PRAGMA table_list('messages_fts')")) if (!_tf_ssb_db_has_rows(db, "PRAGMA table_list('messages_fts')"))
@ -736,13 +735,12 @@ bool tf_ssb_db_blob_store(tf_ssb_t* ssb, const uint8_t* blob, size_t size, char*
return result; return result;
} }
bool tf_ssb_db_get_message_by_author_and_sequence(tf_ssb_t* ssb, const char* author, int64_t sequence, char* out_message_id, size_t out_message_id_size, char* out_previous, bool tf_ssb_db_get_message_by_author_and_sequence(
size_t out_previous_size, char* out_author, size_t out_author_size, double* out_timestamp, char** out_content, char* out_hash, size_t out_hash_size, char* out_signature, tf_ssb_t* ssb, const char* author, int64_t sequence, char* out_message_id, size_t out_message_id_size, double* out_timestamp, char** out_content)
size_t out_signature_size, int* out_flags)
{ {
bool found = false; bool found = false;
sqlite3_stmt* statement; sqlite3_stmt* statement;
const char* query = "SELECT id, previous, author, timestamp, json(content), hash, signature, flags FROM messages WHERE author = ?1 AND sequence = ?2"; const char* query = "SELECT id, timestamp, json(content) FROM messages WHERE author = ?1 AND sequence = ?2";
sqlite3* db = tf_ssb_acquire_db_reader(ssb); sqlite3* db = tf_ssb_acquire_db_reader(ssb);
if (sqlite3_prepare(db, query, -1, &statement, NULL) == SQLITE_OK) if (sqlite3_prepare(db, query, -1, &statement, NULL) == SQLITE_OK)
{ {
@ -750,45 +748,15 @@ bool tf_ssb_db_get_message_by_author_and_sequence(tf_ssb_t* ssb, const char* aut
{ {
if (out_message_id) if (out_message_id)
{ {
snprintf(out_message_id, out_message_id_size, "%s", (const char*)sqlite3_column_text(statement, 0)); strncpy(out_message_id, (const char*)sqlite3_column_text(statement, 0), out_message_id_size - 1);
}
if (out_previous)
{
if (sqlite3_column_type(statement, 1) == SQLITE_NULL)
{
if (out_previous_size)
{
*out_previous = '\0';
}
}
else
{
snprintf(out_previous, out_previous_size, "%s", (const char*)sqlite3_column_text(statement, 1));
}
}
if (out_author)
{
snprintf(out_author, out_author_size, "%s", (const char*)sqlite3_column_text(statement, 2));
} }
if (out_timestamp) if (out_timestamp)
{ {
*out_timestamp = sqlite3_column_double(statement, 3); *out_timestamp = sqlite3_column_double(statement, 1);
} }
if (out_content) if (out_content)
{ {
*out_content = tf_strdup((const char*)sqlite3_column_text(statement, 4)); *out_content = tf_strdup((const char*)sqlite3_column_text(statement, 2));
}
if (out_hash)
{
snprintf(out_hash, out_hash_size, "%s", (const char*)sqlite3_column_text(statement, 5));
}
if (out_signature)
{
snprintf(out_signature, out_signature_size, "%s", (const char*)sqlite3_column_text(statement, 6));
}
if (out_flags)
{
*out_flags = sqlite3_column_int(statement, 7);
} }
found = true; found = true;
} }
@ -1666,7 +1634,6 @@ bool tf_ssb_db_register_account(tf_ssb_t* ssb, const char* name, const char* pas
{ {
if (sqlite3_bind_text(statement, 1, value, value_length, NULL) == SQLITE_OK) if (sqlite3_bind_text(statement, 1, value, value_length, NULL) == SQLITE_OK)
{ {
tf_printf("added user to properties\n");
result = sqlite3_step(statement) == SQLITE_DONE; result = sqlite3_step(statement) == SQLITE_DONE;
} }
sqlite3_finalize(statement); sqlite3_finalize(statement);
@ -1817,65 +1784,3 @@ void tf_ssb_db_resolve_index_async(tf_ssb_t* ssb, const char* host, void (*callb
}; };
tf_ssb_run_work(ssb, _tf_ssb_db_resolve_index_work, _tf_ssb_db_resolve_index_after_work, request); tf_ssb_run_work(ssb, _tf_ssb_db_resolve_index_work, _tf_ssb_db_resolve_index_after_work, request);
} }
bool tf_ssb_db_verify(tf_ssb_t* ssb, const char* id)
{
JSContext* context = tf_ssb_get_context(ssb);
bool verified = true;
int64_t sequence = -1;
if (tf_ssb_db_get_latest_message_by_author(ssb, id, &sequence, NULL, 0))
{
for (int64_t i = 1; i <= sequence; i++)
{
char message_id[k_id_base64_len];
char previous[256];
double timestamp;
char* content = NULL;
char hash[32];
char signature[256];
int flags = 0;
if (tf_ssb_db_get_message_by_author_and_sequence(ssb, id, i, message_id, sizeof(message_id), previous, sizeof(previous), NULL, 0, &timestamp, &content, hash,
sizeof(hash), signature, sizeof(signature), &flags))
{
JSValue message = tf_ssb_format_message(context, previous, id, i, timestamp, hash, content, signature, flags);
char calculated_id[k_id_base64_len];
char extracted_signature[256];
int calculated_flags = 0;
if (!tf_ssb_verify_and_strip_signature(context, message, calculated_id, sizeof(calculated_id), extracted_signature, sizeof(extracted_signature), &calculated_flags))
{
tf_printf("author=%s sequence=%" PRId64 " verify failed.\n", id, i);
verified = false;
}
if (calculated_flags != flags)
{
tf_printf("author=%s sequence=%" PRId64 " flag mismatch %d => %d.\n", id, i, flags, calculated_flags);
verified = false;
}
if (strcmp(message_id, calculated_id))
{
tf_printf("author=%s sequence=%" PRId64 " id mismatch %s => %s.\n", id, i, message_id, calculated_id);
verified = false;
}
JS_FreeValue(context, message);
tf_free(content);
if (!verified)
{
break;
}
}
else
{
tf_printf("Unable to find message with sequence=%" PRId64 " for author=%s.", i, id);
verified = false;
break;
}
}
}
else
{
tf_printf("Unable to get latest message for author '%s'.\n", id);
verified = false;
}
return verified;
}

View File

@ -126,9 +126,8 @@ JSValue tf_ssb_db_get_message_by_id(tf_ssb_t* ssb, const char* id, bool is_keys)
** @param[out] out_content Populated with the message content. Free with tf_free(). ** @param[out] out_content Populated with the message content. Free with tf_free().
** @return True if the message was found and retrieved. ** @return True if the message was found and retrieved.
*/ */
bool tf_ssb_db_get_message_by_author_and_sequence(tf_ssb_t* ssb, const char* author, int64_t sequence, char* out_message_id, size_t out_message_id_size, char* out_previous, bool tf_ssb_db_get_message_by_author_and_sequence(
size_t out_previous_size, char* out_author, size_t out_author_size, double* out_timestamp, char** out_content, char* out_hash, size_t out_hash_size, char* out_signature, tf_ssb_t* ssb, const char* author, int64_t sequence, char* out_message_id, size_t out_message_id_size, double* out_timestamp, char** out_content);
size_t out_signature_size, int* out_flags);
/** /**
** Get information about the last message from an author. ** Get information about the last message from an author.
@ -380,8 +379,6 @@ bool tf_ssb_db_set_property(tf_ssb_t* ssb, const char* id, const char* key, cons
*/ */
void tf_ssb_db_resolve_index_async(tf_ssb_t* ssb, const char* host, void (*callback)(const char* path, void* user_data), void* user_data); void tf_ssb_db_resolve_index_async(tf_ssb_t* ssb, const char* host, void (*callback)(const char* path, void* user_data), void* user_data);
bool tf_ssb_db_verify(tf_ssb_t* ssb, const char* id);
/** /**
** An SQLite authorizer callback. See https://www.sqlite.org/c3ref/set_authorizer.html for use. ** An SQLite authorizer callback. See https://www.sqlite.org/c3ref/set_authorizer.html for use.
** @param user_data User data registered with the authorizer. ** @param user_data User data registered with the authorizer.

View File

@ -399,11 +399,10 @@ static void _tf_ssb_getIdentityInfo_visit(const char* identity, void* data)
identity_info_work_t* request = data; identity_info_work_t* request = data;
request->identities = tf_resize_vec(request->identities, (request->count + 1) * sizeof(char*)); request->identities = tf_resize_vec(request->identities, (request->count + 1) * sizeof(char*));
request->names = tf_resize_vec(request->names, (request->count + 1) * sizeof(char*)); request->names = tf_resize_vec(request->names, (request->count + 1) * sizeof(char*));
char buffer[k_id_base64_len]; request->identities[request->count] = tf_strdup(identity);
snprintf(buffer, sizeof(buffer), "@%s", identity);
request->identities[request->count] = tf_strdup(buffer);
request->names[request->count] = NULL; request->names[request->count] = NULL;
request->count++; request->count++;
;
} }
static void _tf_ssb_getIdentityInfo_work(tf_ssb_t* ssb, void* user_data) static void _tf_ssb_getIdentityInfo_work(tf_ssb_t* ssb, void* user_data)
@ -420,8 +419,8 @@ static void _tf_ssb_getIdentityInfo_work(tf_ssb_t* ssb, void* user_data)
" RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank, " " RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank, "
" messages.content ->> 'name' AS name " " messages.content ->> 'name' AS name "
" FROM messages " " FROM messages "
" JOIN identities ON messages.author = ('@' || identities.public_key) " " JOIN identities ON messages.author = ids.value "
" WHERE identities.user = ? AND json_extract(messages.content, '$.type') = 'about' AND content ->> 'about' = messages.author AND name IS NOT NULL) " " WHERE WHERE identities.user = ? AND json_extract(messages.content, '$.type') = 'about' AND content ->> 'about' = messages.author AND name IS NOT NULL) "
"WHERE author_rank = 1 ", "WHERE author_rank = 1 ",
-1, &statement, NULL); -1, &statement, NULL);
if (request->result == SQLITE_OK) if (request->result == SQLITE_OK)
@ -429,26 +428,22 @@ static void _tf_ssb_getIdentityInfo_work(tf_ssb_t* ssb, void* user_data)
if (sqlite3_bind_text(statement, 1, request->name, -1, NULL) == SQLITE_OK) if (sqlite3_bind_text(statement, 1, request->name, -1, NULL) == SQLITE_OK)
{ {
int r = SQLITE_OK; int r = SQLITE_OK;
while ((r = sqlite3_step(statement)) == SQLITE_ROW) while ((r = sqlite3_step(statement)) == SQLITE_OK)
{ {
const char* identity = (const char*)sqlite3_column_text(statement, 0);
const char* name = (const char*)sqlite3_column_text(statement, 1);
for (int i = 0; i < request->count; i++) for (int i = 0; i < request->count; i++)
{ {
if (!request->names[i] && strcmp(request->identities[i], identity) == 0) const char* identity = (const char*)sqlite3_column_text(statement, 0);
const char* name = (const char*)sqlite3_column_text(statement, 1);
if (strcmp(request->identities[i], identity) == 0 && !request->names[i])
{ {
request->names[i] = tf_strdup(name); request->names[i] = tf_strdup(name);
break;
} }
break;
} }
} }
} }
sqlite3_finalize(statement); sqlite3_finalize(statement);
} }
else
{
tf_printf("prepare failed: %s.\n", sqlite3_errmsg(db));
}
tf_ssb_db_identity_get_active(db, request->name, request->package_owner, request->package_name, request->active_identity, sizeof(request->active_identity)); tf_ssb_db_identity_get_active(db, request->name, request->package_owner, request->package_name, request->active_identity, sizeof(request->active_identity));
if (!*request->active_identity && request->count) if (!*request->active_identity && request->count)
@ -581,6 +576,29 @@ static JSValue _tf_ssb_appendMessageWithIdentity(JSContext* context, JSValueCons
return result; return result;
} }
static JSValue _tf_ssb_getMessage(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue result = JS_NULL;
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
if (ssb)
{
const char* id = JS_ToCString(context, argv[0]);
int64_t sequence = 0;
JS_ToInt64(context, &sequence, argv[1]);
double timestamp = -1.0;
char* contents = NULL;
if (tf_ssb_db_get_message_by_author_and_sequence(ssb, id, sequence, NULL, 0, &timestamp, &contents))
{
result = JS_NewObject(context);
JS_SetPropertyStr(context, result, "timestamp", JS_NewFloat64(context, timestamp));
JS_SetPropertyStr(context, result, "content", JS_NewString(context, contents));
tf_free(contents);
}
JS_FreeCString(context, id);
}
return result;
}
static JSValue _tf_ssb_blobGet(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) static JSValue _tf_ssb_blobGet(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{ {
JSValue result = JS_NULL; JSValue result = JS_NULL;
@ -1873,6 +1891,7 @@ void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
JS_SetPropertyStr(context, object, "getAllIdentities", JS_NewCFunction(context, _tf_ssb_getAllIdentities, "getAllIdentities", 0)); JS_SetPropertyStr(context, object, "getAllIdentities", JS_NewCFunction(context, _tf_ssb_getAllIdentities, "getAllIdentities", 0));
JS_SetPropertyStr(context, object, "getActiveIdentity", JS_NewCFunction(context, _tf_ssb_getActiveIdentity, "getActiveIdentity", 3)); JS_SetPropertyStr(context, object, "getActiveIdentity", JS_NewCFunction(context, _tf_ssb_getActiveIdentity, "getActiveIdentity", 3));
JS_SetPropertyStr(context, object, "getIdentityInfo", JS_NewCFunction(context, _tf_ssb_getIdentityInfo, "getIdentityInfo", 3)); JS_SetPropertyStr(context, object, "getIdentityInfo", JS_NewCFunction(context, _tf_ssb_getIdentityInfo, "getIdentityInfo", 3));
JS_SetPropertyStr(context, object, "getMessage", JS_NewCFunction(context, _tf_ssb_getMessage, "getMessage", 2));
JS_SetPropertyStr(context, object, "blobGet", JS_NewCFunction(context, _tf_ssb_blobGet, "blobGet", 1)); JS_SetPropertyStr(context, object, "blobGet", JS_NewCFunction(context, _tf_ssb_blobGet, "blobGet", 1));
JS_SetPropertyStr(context, object, "messageContentGet", JS_NewCFunction(context, _tf_ssb_messageContentGet, "messageContentGet", 1)); JS_SetPropertyStr(context, object, "messageContentGet", JS_NewCFunction(context, _tf_ssb_messageContentGet, "messageContentGet", 1));
JS_SetPropertyStr(context, object, "connections", JS_NewCFunction(context, _tf_ssb_connections, "connections", 0)); JS_SetPropertyStr(context, object, "connections", JS_NewCFunction(context, _tf_ssb_connections, "connections", 0));

View File

@ -83,13 +83,6 @@ try:
driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))) driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))))
id1 = wait.until(expected_conditions.presence_of_element_located((By.TAG_NAME, 'li'))).text.split(' ')[-1] id1 = wait.until(expected_conditions.presence_of_element_located((By.TAG_NAME, 'li'))).text.split(' ')[-1]
driver.get('http://localhost:8888/~core/admin/')
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
driver.switch_to.frame(driver.find_element(By.ID, 'document'))
wait.until(expected_conditions.presence_of_element_located((By.ID, 'gs_room_name'))).send_keys('test room')
wait.until(expected_conditions.presence_of_element_located((By.XPATH, '//*[@id="gs_room_name"]/following-sibling::button'))).click()
driver.switch_to.alert.accept()
driver.get('http://localhost:8888') driver.get('http://localhost:8888')
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))) wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
driver.switch_to.frame(driver.find_element(By.ID, 'document')) driver.switch_to.frame(driver.find_element(By.ID, 'document'))
@ -113,15 +106,9 @@ try:
except: except:
pass pass
# WebDriverException (shadow root is detached) tf_tab_news = wait.until(exists_in_shadow_root(tf_app, By.ID, 'tf-tab-news')).shadow_root
while True: tf_tab_news.find_element(By.ID, 'tf-compose').shadow_root.find_element(By.ID, 'edit').send_keys('Hello, world!')
try: tf_tab_news.find_element(By.ID, 'tf-compose').shadow_root.find_element(By.ID, 'submit').click()
tf_tab_news = wait.until(exists_in_shadow_root(tf_app, By.ID, 'tf-tab-news')).shadow_root
tf_tab_news.find_element(By.ID, 'tf-compose').shadow_root.find_element(By.ID, 'edit').send_keys('Hello, world!')
tf_tab_news.find_element(By.ID, 'tf-compose').shadow_root.find_element(By.ID, 'submit').click()
break
except:
pass
driver.switch_to.default_content() driver.switch_to.default_content()
driver.find_element(By.ID, 'allow').click() driver.find_element(By.ID, 'allow').click()