You've already forked tribes-plugin-trust
Introduce the Trust plugin as the federation provider for tribe identity and hello handshakes, with synced Ash resources for remote tribes and tribe relationships plus an admin LiveView for trust management.
This commit is contained in:
+37
@@ -0,0 +1,37 @@
|
||||
%{
|
||||
configs: [
|
||||
%{
|
||||
name: "default",
|
||||
files: %{
|
||||
included: ["lib/", "src/", "test/"],
|
||||
excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"]
|
||||
},
|
||||
plugins: [],
|
||||
requires: [],
|
||||
strict: false,
|
||||
parse_timeout: 5000,
|
||||
color: true,
|
||||
checks: [
|
||||
{Credo.Check.Consistency.ExceptionNames, []},
|
||||
{Credo.Check.Consistency.LineEndings, []},
|
||||
{Credo.Check.Consistency.ParameterPatternMatching, []},
|
||||
{Credo.Check.Consistency.SpaceAroundOperators, []},
|
||||
{Credo.Check.Consistency.SpaceInParentheses, []},
|
||||
{Credo.Check.Consistency.TabsOrSpaces, []},
|
||||
{Credo.Check.Design.AliasUsage, false},
|
||||
{Credo.Check.Design.TagTODO, [exit_status: 2]},
|
||||
{Credo.Check.Design.TagFIXME, []},
|
||||
{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
|
||||
{Credo.Check.Refactor.CyclomaticComplexity, false},
|
||||
{Credo.Check.Refactor.FunctionArity, [max_arity: 13]},
|
||||
{Credo.Check.Refactor.LongQuoteBlocks, false},
|
||||
{Credo.Check.Refactor.MapInto, false},
|
||||
{Credo.Check.Refactor.Nesting, [max_nesting: 7]},
|
||||
{Credo.Check.Warning.IoInspect, []},
|
||||
{Credo.Check.Warning.LazyLogging, false},
|
||||
{Credo.Check.Warning.MixEnv, false},
|
||||
{Credo.Check.Warning.UnsafeExec, []}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export DIRENV_WARN_TIMEOUT=20s
|
||||
|
||||
eval "$(devenv direnvrc)"
|
||||
|
||||
use devenv
|
||||
@@ -0,0 +1,3 @@
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
||||
@@ -0,0 +1,71 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
pull_request:
|
||||
branches: [main, master]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 5s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
|
||||
env:
|
||||
MIX_ENV: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: erlef/setup-beam@v1
|
||||
with:
|
||||
elixir-version: "1.19"
|
||||
otp-version: "27"
|
||||
|
||||
- name: Cache deps
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
deps
|
||||
_build
|
||||
key: ${{ runner.os }}-mix-${{ hashFiles('mix.lock') }}
|
||||
restore-keys: ${{ runner.os }}-mix-
|
||||
|
||||
- name: Install dependencies
|
||||
run: mix deps.get
|
||||
|
||||
- name: Compile (warnings as errors)
|
||||
run: mix compile --warnings-as-errors
|
||||
|
||||
- name: Check formatting
|
||||
run: mix format --check-formatted
|
||||
|
||||
- name: Run tests
|
||||
run: scripts/plugin test
|
||||
|
||||
- name: Validate manifest
|
||||
run: |
|
||||
elixir -e '
|
||||
manifest = "manifest.json" |> File.read!() |> JSON.decode!()
|
||||
required = ["id", "slug", "version", "entry_module", "host_api", "provides", "requires"]
|
||||
missing = Enum.filter(required, &(not Map.has_key?(manifest, &1)))
|
||||
if missing != [] do
|
||||
IO.puts(:stderr, "Missing manifest fields: #{inspect(missing)}")
|
||||
System.halt(1)
|
||||
end
|
||||
IO.puts("Plugin: #{manifest["id"]} (#{manifest["slug"]}) v#{manifest["version"]}")
|
||||
'
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
# Dependencies and build
|
||||
/deps/
|
||||
/_build/
|
||||
/dist/
|
||||
/result
|
||||
/result-*
|
||||
|
||||
# Devenv
|
||||
.devenv*
|
||||
devenv.local.nix
|
||||
|
||||
# Pre-commit
|
||||
.pre-commit-config.yaml
|
||||
|
||||
# Node
|
||||
/node_modules/
|
||||
/assets/node_modules/
|
||||
|
||||
# Built plugin assets
|
||||
/priv/static/*.css
|
||||
/priv/static/*.js
|
||||
/priv/static/vendor/
|
||||
|
||||
# Browser tooling
|
||||
.playwright-mcp/
|
||||
|
||||
# Generated on crash
|
||||
erl_crash.dump
|
||||
|
||||
# Editor and OS
|
||||
.elixir_ls/
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Env files
|
||||
.env
|
||||
.env.local
|
||||
@@ -0,0 +1,28 @@
|
||||
# Tribes Plugin Agent Guide
|
||||
|
||||
This repository is a Tribes plugin. Preserve generated names unless a task explicitly asks to rename the plugin.
|
||||
|
||||
## Required Workflow
|
||||
|
||||
- Use `plugin` for plugin-aware commands in the devenv shell: `plugin validate`, `plugin test`, `plugin precommit`, and `plugin smoke`. Outside the devenv shell, use the local `scripts/plugin` wrapper.
|
||||
- Use `devenv shell -- <command>` when installing or building Node assets from outside the devenv shell. Do not run raw `npm install` directly.
|
||||
- Do not use raw `mix test` for host-backed plugin suites unless you are deliberately recreating the `plugin test` host environment by hand; use `mix raw_test` only for that low-level debugging path.
|
||||
- For AshPostgres resource changes, generate migrations with `plugin ash.codegen <name>`. Use `plugin ecto.migration <name>` only for rare manual schema migrations that are not derived from Ash resources.
|
||||
- Do not edit an existing migration after it may have run; add a new migration instead.
|
||||
- Run `scripts/plugin smoke` after changing `mix.exs`, `manifest.json`, entry modules, or host-facing dependency setup.
|
||||
- Keep commits semantic, for example `feat: add plugin smoke checks`, and include a body except for minimal patches.
|
||||
|
||||
## Plugin Contract
|
||||
|
||||
- `manifest.json` `entry_module` must point at the plugin entry module.
|
||||
- The dev `:tribes` dependency is compile-only so host plugin loading can build `_build/dev` beams without starting a nested host app.
|
||||
- The test `:tribes` dependency is runtime-enabled so host-backed tests can start `Tribes.Repo` and related services.
|
||||
- If the plugin adds cluster-synced Ash resources, use `AshNostrSync` deliberately and document replication semantics in `docs/plugin-contract.md`.
|
||||
|
||||
## Frontend
|
||||
|
||||
- Default pages should render inside host chrome with `Tribes.Plugin.Layouts.app`; keep `org.tribe-one.caps.ui@1` in `manifest.json` `requires` for host chrome or direct `Tribes.UI` usage.
|
||||
- Prefer TypeScript in `assets/ts` for browser code. The default build emits browser-ready files to `priv/static`.
|
||||
- Register plugin LiveView hooks from `manifest.json` `assets.global_js` bundles via `window.TribesPluginHooks`. The host does not auto-import external plugin `phoenix-colocated/<otp_app>` modules into its `app.js` bundle.
|
||||
- Keep CSS selectors plugin-scoped to avoid collisions with the host or other plugins.
|
||||
- Use the package scripts in `assets/package.json`; do not bypass them with ad-hoc asset commands.
|
||||
@@ -0,0 +1,208 @@
|
||||
# Trust
|
||||
|
||||
Tribe-to-tribe alliances and trust
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Edit `manifest.json` - set description, capabilities, requirements
|
||||
2. Implement your plugin in `lib/trust/plugin.ex`
|
||||
3. Run validation, smoke checks, and tests:
|
||||
|
||||
```bash
|
||||
mix deps.get
|
||||
mix tribes.plugin.validate
|
||||
scripts/plugin smoke
|
||||
scripts/plugin test
|
||||
```
|
||||
|
||||
If you are using the shared `tribes` devenv, run through the local helper instead:
|
||||
|
||||
```bash
|
||||
devenv shell -- plugin validate
|
||||
devenv shell -- plugin smoke
|
||||
devenv shell -- plugin test
|
||||
devenv shell -- plugin precommit
|
||||
```
|
||||
|
||||
Generate AshPostgres resource migrations through the same wrapper so they
|
||||
target the host `Tribes.Repo` and the plugin migration directory:
|
||||
|
||||
```bash
|
||||
plugin ash.codegen update_example_resources
|
||||
```
|
||||
|
||||
Manual Ecto migrations are the rare exception for schema work not derived
|
||||
from Ash resources:
|
||||
|
||||
```bash
|
||||
plugin ecto.migration add_example_table
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
For local development alongside a Tribes checkout:
|
||||
|
||||
```bash
|
||||
# Symlink into the host plugins directory once
|
||||
cd ../tribes
|
||||
ln -s ../tribes-plugin-trust plugins/trust
|
||||
|
||||
# Start Tribes dev server
|
||||
iex --sname dev -S mix phx.server
|
||||
```
|
||||
|
||||
Then edit the plugin in its own repo. In development, the host watches
|
||||
symlinked external plugins and automatically:
|
||||
|
||||
- runs `mix compile` for Elixir/HEEx or manifest changes
|
||||
- runs `npm run build --prefix assets` when `assets/package.json` is present and plugin asset files change
|
||||
- reloads the plugin in the running Tribes VM
|
||||
- triggers a Phoenix browser reload after the rebuild finishes
|
||||
|
||||
Browser hooks for external plugins should live in plugin JS assets declared
|
||||
in `manifest.json` `assets.global_js` and register themselves through
|
||||
`window.TribesPluginHooks`. Phoenix colocated hooks are not auto-imported
|
||||
from external plugin OTP apps by the host `app.js` bundle.
|
||||
|
||||
That means the normal edit/compile loop is:
|
||||
|
||||
```bash
|
||||
cd /path/to/tribes-plugin-trust
|
||||
mix deps.get
|
||||
|
||||
# in another terminal
|
||||
cd /path/to/tribes
|
||||
iex --sname dev -S mix phx.server
|
||||
```
|
||||
|
||||
Inside the plugin repo's devenv shell, the `plugin` helper forwards to the host devenv automatically:
|
||||
|
||||
```bash
|
||||
plugin validate
|
||||
plugin smoke
|
||||
plugin test
|
||||
plugin precommit
|
||||
```
|
||||
|
||||
If your plugin does not need a custom frontend pipeline, you can skip
|
||||
`assets/package.json` and write browser-ready files directly under `assets/js`
|
||||
and `assets/css`; the host dev watcher will copy them into `priv/static` for
|
||||
you in development. The default generator uses TypeScript under `assets/ts`.
|
||||
|
||||
## LiveView Hooks
|
||||
|
||||
External plugins do not participate in the host asset bundle, so the host
|
||||
cannot statically import arbitrary plugin `phoenix-colocated/<otp_app>`
|
||||
modules. Register browser hooks from your plugin JS instead:
|
||||
|
||||
```typescript
|
||||
window.TribesPluginHooks = window.TribesPluginHooks ?? {};
|
||||
window.TribesPluginHooks.TribeOneTribesPluginTrustExample = {
|
||||
mounted() {
|
||||
console.info("trust hook mounted");
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Keep the compiled JS file listed in `manifest.json` `assets.global_js` so it
|
||||
loads before the host LiveSocket connects.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
trust/
|
||||
|-- manifest.json # Plugin metadata (Nix build + runtime)
|
||||
|-- mix.exs # Dependencies
|
||||
|-- config/ # Host-backed test config
|
||||
|-- lib/
|
||||
| |-- trust/
|
||||
| | |-- plugin.ex # Tribes.Plugin entry point
|
||||
| | `-- application.ex # OTP supervision tree (optional)
|
||||
| `-- trust_web/
|
||||
| `-- live/ # LiveView pages
|
||||
|-- assets/ # TS/CSS (one bundle per plugin)
|
||||
| |-- ts/ # TypeScript browser entry points
|
||||
| |-- package.json # Optional build script used by dev + Guix packaging
|
||||
| |-- tsconfig.json # TypeScript compiler settings
|
||||
| `-- package-lock.json # Optional, recommended for reproducible builds
|
||||
|-- priv/
|
||||
| |-- static/ # Built assets for release
|
||||
| `-- repo/migrations/ # Ecto migrations
|
||||
|-- scripts/
|
||||
| `-- plugin # Shared devenv/non-devenv test wrapper
|
||||
`-- test/
|
||||
```
|
||||
|
||||
## Manifest
|
||||
|
||||
`manifest.json` declares your plugin's identity and capabilities:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "org.tribe-one.plugins.trust",
|
||||
"slug": "trust",
|
||||
"display_name": "Trust",
|
||||
"entry_module": "TribeOne.TribesPlugin.Trust.Plugin",
|
||||
"host_api": "1",
|
||||
"otp_app": "tribe_one_trust",
|
||||
"provides": ["org.tribe-one.caps.some_capability@1"],
|
||||
"requires": ["org.tribe-one.caps.ui@1"],
|
||||
"enhances_with": ["org.tribe-one.caps.inference@1"]
|
||||
}
|
||||
```
|
||||
|
||||
- **entry_module** - must be a valid module ending in `.Plugin`
|
||||
- **otp_app** - required OTP application name; vendor-prefix it to avoid collisions
|
||||
- **provides** - capabilities this plugin makes available
|
||||
- **requires** - hard plugin/API contracts (build fails without them)
|
||||
- **enhances_with** - optional dependencies (plugin degrades gracefully)
|
||||
|
||||
The default generator includes `org.tribe-one.caps.ui@1` because the generated LiveView renders
|
||||
inside the host chrome through `Tribes.Plugin.Layouts.app`. Keep `org.tribe-one.caps.ui@1` when a
|
||||
plugin uses host chrome, imports `Tribes.UI.Components`, or uses `use Tribes.UI`.
|
||||
|
||||
## Plugin Data
|
||||
|
||||
If your plugin adds Ash resources that should replicate cluster-wide, use
|
||||
`extensions: [AshNostrSync]` on those resources and assign a sync lane per
|
||||
resource. The host default lane is `bulk`; latency-sensitive topology or
|
||||
orchestration resources should opt into `control`. Keep resource-specific
|
||||
side effects explicit in actions.
|
||||
|
||||
## Testing
|
||||
|
||||
The generated plugin starts with two host-backed test layers:
|
||||
|
||||
- **Contract tests** (`test/trust/plugin_contract_test.exs`) - manifest and runtime spec stay aligned
|
||||
- **Page tests** (`test/trust/home_page_test.exs`) - the plugin renders through the real host page pipeline
|
||||
|
||||
Run them with:
|
||||
|
||||
```bash
|
||||
mix tribes.plugin.validate
|
||||
scripts/plugin smoke
|
||||
scripts/plugin test
|
||||
```
|
||||
|
||||
Use `plugin test` / `plugin precommit` for host-backed tests. The helper invokes
|
||||
Mix with the host database/services and host plugin-manager paths, including
|
||||
built-in providers such as `tribes_ui`. Raw `mix test` in a plugin checkout is
|
||||
guarded and prints this guidance; `mix raw_test` / `mix raw_precommit` are
|
||||
available only for unusual manual debugging when you have set the same host
|
||||
environment yourself.
|
||||
|
||||
## Building for Release
|
||||
|
||||
```bash
|
||||
MIX_ENV=prod mix compile
|
||||
devenv shell -- npm run build --prefix assets
|
||||
|
||||
mkdir -p dist/trust
|
||||
cp -r _build/prod/lib/tribe_one_trust/ebin dist/trust/
|
||||
cp -r priv dist/trust/
|
||||
cp manifest.json dist/trust/
|
||||
```
|
||||
|
||||
## Licence
|
||||
|
||||
TODO: Choose a licence.
|
||||
@@ -0,0 +1 @@
|
||||
min-release-age=7
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Plugin CSS entry point.
|
||||
*
|
||||
* Served at /plugins-assets/trust/trust.css
|
||||
* and included in the page layout if declared in manifest.json assets.global_css.
|
||||
*
|
||||
* Prefix all selectors with your plugin name to avoid collisions
|
||||
* with host or other plugin styles.
|
||||
*/
|
||||
|
||||
.trust {
|
||||
/* Plugin-scoped styles go here */
|
||||
}
|
||||
Generated
+514
@@ -0,0 +1,514 @@
|
||||
{
|
||||
"name": "trust-assets",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "trust-assets",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.28.0",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
|
||||
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
|
||||
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
|
||||
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
|
||||
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
|
||||
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
|
||||
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
|
||||
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
|
||||
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
|
||||
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
|
||||
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
|
||||
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.28.0",
|
||||
"@esbuild/android-arm": "0.28.0",
|
||||
"@esbuild/android-arm64": "0.28.0",
|
||||
"@esbuild/android-x64": "0.28.0",
|
||||
"@esbuild/darwin-arm64": "0.28.0",
|
||||
"@esbuild/darwin-x64": "0.28.0",
|
||||
"@esbuild/freebsd-arm64": "0.28.0",
|
||||
"@esbuild/freebsd-x64": "0.28.0",
|
||||
"@esbuild/linux-arm": "0.28.0",
|
||||
"@esbuild/linux-arm64": "0.28.0",
|
||||
"@esbuild/linux-ia32": "0.28.0",
|
||||
"@esbuild/linux-loong64": "0.28.0",
|
||||
"@esbuild/linux-mips64el": "0.28.0",
|
||||
"@esbuild/linux-ppc64": "0.28.0",
|
||||
"@esbuild/linux-riscv64": "0.28.0",
|
||||
"@esbuild/linux-s390x": "0.28.0",
|
||||
"@esbuild/linux-x64": "0.28.0",
|
||||
"@esbuild/netbsd-arm64": "0.28.0",
|
||||
"@esbuild/netbsd-x64": "0.28.0",
|
||||
"@esbuild/openbsd-arm64": "0.28.0",
|
||||
"@esbuild/openbsd-x64": "0.28.0",
|
||||
"@esbuild/openharmony-arm64": "0.28.0",
|
||||
"@esbuild/sunos-x64": "0.28.0",
|
||||
"@esbuild/win32-arm64": "0.28.0",
|
||||
"@esbuild/win32-ia32": "0.28.0",
|
||||
"@esbuild/win32-x64": "0.28.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "trust-assets",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"build": "npm run check && mkdir -p ../priv/static && cp -r css/. ../priv/static && esbuild ts/trust.ts --bundle --target=es2022 --format=iife --outfile=../priv/static/trust.js",
|
||||
"check": "tsc --project tsconfig.json --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.28.0",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Plugin TypeScript entry point.
|
||||
//
|
||||
// This file is compiled to /priv/static/trust.js, served by the host at
|
||||
// /plugins-assets/trust/trust.js, and included when declared in
|
||||
// manifest.json assets.global_js.
|
||||
//
|
||||
// Register plugin LiveView hooks here. External plugin OTP apps are not
|
||||
// auto-imported by the host's phoenix-colocated bundle.
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
TribesPluginHooks?: Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
|
||||
window.TribesPluginHooks = window.TribesPluginHooks ?? {};
|
||||
|
||||
// Example:
|
||||
// window.TribesPluginHooks.TribeOneTribesPluginTrustExample = {
|
||||
// mounted() {
|
||||
// console.info("trust hook mounted");
|
||||
// },
|
||||
// };
|
||||
|
||||
console.info("trust loaded");
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "../priv/static",
|
||||
"rootDir": "ts"
|
||||
},
|
||||
"include": ["ts/**/*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import Config
|
||||
|
||||
config :tribe_one_trust, ash_domains: [TribeOne.TribesPlugin.Trust.Domain]
|
||||
|
||||
import_config "#{config_env()}.exs"
|
||||
@@ -0,0 +1,5 @@
|
||||
import Config
|
||||
|
||||
# The host is a compile-only dependency in dev. Suppress host Ash domain
|
||||
# config inclusion warnings while compiling host modules as a dependency.
|
||||
config :ash, :validate_domain_config_inclusion?, false
|
||||
@@ -0,0 +1 @@
|
||||
import Config
|
||||
@@ -0,0 +1,3 @@
|
||||
import Config
|
||||
|
||||
import_config "../../tribes/config/config.exs"
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"nodes": {
|
||||
"devenv": {
|
||||
"locked": {
|
||||
"dir": "src/modules",
|
||||
"lastModified": 1779899848,
|
||||
"narHash": "sha256-Rch5lM6yNoKUjsPd0jb4db3XCm0Q5PZ+eNsgnYKOlBQ=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"rev": "7c4c690d5aa3cb94a9cb7bd7172f8739c408a343",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"dir": "src/modules",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1767039857,
|
||||
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
||||
"owner": "NixOS",
|
||||
"repo": "flake-compat",
|
||||
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"git-hooks": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1778507602,
|
||||
"narHash": "sha256-kTwur1wV+01SdqskVMSo6JMEpg71ps3HpbFY2GsflKs=",
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"rev": "61ab0e80d9c7ab14c256b5b453d8b3fb0189ba0a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"git-hooks",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709087332,
|
||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"inputs": {
|
||||
"nixpkgs-src": "nixpkgs-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1778507786,
|
||||
"narHash": "sha256-HzSQCKMsMr8r55LwM1JuzIOB+8bzk0FEv6sItKvsfoY=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv-nixpkgs",
|
||||
"rev": "8f24a228a782e24576b155d1e39f0d914b380691",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"ref": "rolling",
|
||||
"repo": "devenv-nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1778274207,
|
||||
"narHash": "sha256-I4puXmX1iovcCHZlRmztO3vW0mAbbRvq4F8wgIMQ1MM=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "b3da656039dc7a6240f27b2ef8cc6a3ef3bccae7",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"devenv": "devenv",
|
||||
"git-hooks": "git-hooks",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: {
|
||||
env = {
|
||||
MIX_OS_DEPS_COMPILE_PARTITION_COUNT = 8;
|
||||
NODE_ENV = "development";
|
||||
# Delay npm dependency resolution to reduce rushed supply-chain updates.
|
||||
NPM_CONFIG_MIN_RELEASE_AGE = "7";
|
||||
};
|
||||
|
||||
packages = with pkgs;
|
||||
[
|
||||
git
|
||||
alejandra
|
||||
prettier
|
||||
]
|
||||
++ lib.optionals pkgs.stdenv.isLinux [
|
||||
inotify-tools
|
||||
];
|
||||
|
||||
languages = {
|
||||
elixir = {
|
||||
enable = true;
|
||||
package = pkgs.elixir_1_19;
|
||||
};
|
||||
|
||||
javascript = {
|
||||
enable = true;
|
||||
package = pkgs.nodejs_24;
|
||||
npm.enable = true;
|
||||
};
|
||||
};
|
||||
|
||||
dotenv.enable = true;
|
||||
devenv.warnOnNewVersion = false;
|
||||
|
||||
git-hooks.hooks = {
|
||||
alejandra.enable = true;
|
||||
prettier.enable = true;
|
||||
prettier.files = "\\.(js|ts|tsx|css)$";
|
||||
check-added-large-files = {
|
||||
enable = true;
|
||||
args = ["--maxkb=131072"];
|
||||
};
|
||||
mix-format.enable = true;
|
||||
mix-format.files = "\\.(ex|exs|heex)$";
|
||||
};
|
||||
|
||||
enterShell = ''
|
||||
echo
|
||||
elixir --version
|
||||
echo -n "Node.js "
|
||||
node --version
|
||||
echo
|
||||
'';
|
||||
|
||||
scripts = {
|
||||
plugin.exec = ''bash "$DEVENV_ROOT/scripts/plugin" "$@"'';
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json
|
||||
inputs:
|
||||
nixpkgs:
|
||||
url: github:cachix/devenv-nixpkgs/rolling
|
||||
git-hooks:
|
||||
url: github:cachix/git-hooks.nix
|
||||
inputs:
|
||||
nixpkgs:
|
||||
follows: nixpkgs
|
||||
@@ -0,0 +1,17 @@
|
||||
# Checklist
|
||||
|
||||
## After Generation
|
||||
|
||||
- Confirm `manifest.json` `id`, `slug`, `otp_app`, and `entry_module`.
|
||||
- Confirm `manifest.json` `entry_module` matches your plugin module.
|
||||
- Run `mix deps.get`.
|
||||
- Run `scripts/plugin smoke`.
|
||||
- Run `scripts/plugin test`.
|
||||
|
||||
## Before Committing
|
||||
|
||||
- Run `mix format`.
|
||||
- Run `scripts/plugin precommit`.
|
||||
- If assets changed, run `devenv shell -- npm run build --prefix assets`.
|
||||
- Check `git status --short` for generated files that should not be committed.
|
||||
- Commit with a semantic subject and a useful body unless the patch is minimal.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Development
|
||||
|
||||
## Local Setup
|
||||
|
||||
This plugin expects a sibling Tribes checkout at `../tribes`.
|
||||
|
||||
```bash
|
||||
mix deps.get
|
||||
devenv shell -- plugin test
|
||||
```
|
||||
|
||||
To load the plugin in the host during development:
|
||||
|
||||
```bash
|
||||
cd ../tribes
|
||||
ln -s ../tribes-plugin-trust plugins/trust
|
||||
iex --sname dev -S mix phx.server
|
||||
```
|
||||
|
||||
Set `TRIBES_HOST_ROOT=/path/to/tribes` if your host checkout is not a sibling.
|
||||
|
||||
## Command Wrapper
|
||||
|
||||
Use `scripts/plugin` for host-aware workflows:
|
||||
|
||||
```bash
|
||||
scripts/plugin validate
|
||||
scripts/plugin test
|
||||
scripts/plugin precommit
|
||||
scripts/plugin ash.codegen update_example_resources
|
||||
scripts/plugin ecto.migration add_example_table
|
||||
scripts/plugin smoke
|
||||
```
|
||||
|
||||
Inside the plugin devenv shell, the `plugin` command is available and forwards
|
||||
through the same wrapper.
|
||||
|
||||
Prefer `plugin test` over raw `mix test` for host-backed plugin suites. The
|
||||
wrapper runs Mix with the host database/services and points the host plugin
|
||||
manager at the host manifest plus built-in providers such as `tribes_ui`. Use
|
||||
`mix raw_test` / `mix raw_precommit` only for deliberate low-level debugging.
|
||||
|
||||
## Assets
|
||||
|
||||
Browser code lives in `assets/ts` and is compiled to `priv/static`:
|
||||
|
||||
```bash
|
||||
devenv shell -- npm run build --prefix assets
|
||||
```
|
||||
|
||||
Use the wrapper form for installs too:
|
||||
|
||||
```bash
|
||||
devenv shell -- npm install --prefix assets
|
||||
```
|
||||
@@ -0,0 +1,90 @@
|
||||
# Plugin Contract
|
||||
|
||||
## Manifest
|
||||
|
||||
`manifest.json` is the runtime contract consumed by Tribes:
|
||||
|
||||
- `id` is the canonical reverse-DNS plugin identity; `slug` is the local URL/assets namespace.
|
||||
- `otp_app` should be vendor-prefixed and must match the Mix application name.
|
||||
- `entry_module` must be a loadable module ending in `.Plugin`.
|
||||
- `provides` declares capabilities exported by the plugin.
|
||||
- `requires` declares hard plugin/API contracts beyond the `host_api` foundation.
|
||||
- `enhances_with` declares optional host capabilities.
|
||||
- `migrations` should be `true` when `priv/repo/migrations` contains plugin migrations.
|
||||
- `children` should be `true` when the plugin starts its own supervision tree.
|
||||
|
||||
`host_api` is the versioned foundation contract. It provides the supported
|
||||
Phoenix, Ash, PubSub, data, and cluster-event APIs for plugins. Do not add
|
||||
separate framework capability requirements for APIs that belong to that
|
||||
foundation.
|
||||
|
||||
Declare `org.tribe-one.caps.ui@1` in `requires` when rendering through `Tribes.Plugin.Layouts.app`,
|
||||
importing `Tribes.UI.Components`, or using `use Tribes.UI`.
|
||||
|
||||
## Entry Modules
|
||||
|
||||
The plugin entry module is:
|
||||
|
||||
```elixir
|
||||
defmodule TribeOne.TribesPlugin.Trust.Plugin do
|
||||
use Tribes.Plugin.Base, otp_app: :tribe_one_trust
|
||||
end
|
||||
```
|
||||
|
||||
Keep plugin implementation code under your own namespace. The default
|
||||
generator uses `TribeOne.TribesPlugin.*`; third-party plugins should use their own
|
||||
organization or project namespace.
|
||||
|
||||
## Host Dependencies
|
||||
|
||||
`tribes_plugin_api` is the release-facing `host_api` foundation. It carries
|
||||
the supported plugin behaviours, helpers, Phoenix/Ash data surface, and sync
|
||||
DSL. Do not add the full `:tribes` app as a production dependency.
|
||||
|
||||
Host services are exposed through public facade modules in
|
||||
`Tribes.Plugin.Services`. Use `Tribes.Plugin.Services.Alliance` for local
|
||||
tribe metadata and tribe users, `Tribes.Plugin.Services.Metrics` for compact
|
||||
metric rollups, and `Tribes.Plugin.Services.Logs` for operational plugin
|
||||
logging.
|
||||
|
||||
## Localization
|
||||
|
||||
Keep plugin-specific copy in this plugin's own Gettext backend and
|
||||
`priv/gettext` catalogs. For shared Tribes UI labels that are documented as
|
||||
public plugin API, import `Tribes.Plugin.Gettext` and call `tgettext/2`:
|
||||
|
||||
```elixir
|
||||
import Tribes.Plugin.Gettext
|
||||
|
||||
tgettext("Cancel")
|
||||
tgettext("Save")
|
||||
```
|
||||
|
||||
Do not call arbitrary internal Tribes Gettext domains from plugin code.
|
||||
|
||||
The generated plugin intentionally splits the `:tribes` path dependency by
|
||||
environment:
|
||||
|
||||
- In `:dev`, `:tribes` is compile-only. This lets `mix compile` create the
|
||||
entry-module beam expected by the host plugin manager without starting a
|
||||
nested Tribes application.
|
||||
- In `:test`, `:tribes` is runtime-enabled. Host-backed tests need the real
|
||||
repository, endpoint pipeline, and plugin contract helpers.
|
||||
|
||||
## Ash Resources
|
||||
|
||||
For cluster-synced plugin data, add Ash resources under the plugin namespace
|
||||
and use `extensions: [AshNostrSync]` only for data that should replicate
|
||||
across the cluster. Prefer one UUID primary key per synced resource. Document
|
||||
any local-only tables, retention policies, or side effects in plugin docs.
|
||||
|
||||
This plugin owns the tribe-to-tribe trust provider for
|
||||
`org.tribes.federation.provider@1` and stores:
|
||||
|
||||
- `RemoteTribe` — signed or locally observed remote tribe identity metadata.
|
||||
- `TribeRelationship` — local relationship state, trust score, notes, requested
|
||||
capabilities, and granted capabilities.
|
||||
|
||||
Both resources are cluster-synced because they are tribe-level governance state.
|
||||
They are not internal cluster trust records and must not be used to authorize
|
||||
AshNostrSync node writers.
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
defmodule TribeOne.TribesPlugin.Trust do
|
||||
@moduledoc false
|
||||
|
||||
alias TribeOne.TribesPlugin.Trust.Domain
|
||||
|
||||
@federation_capabilities [
|
||||
"org.tribes.federation.hello@1",
|
||||
"org.tribes.federation.relationship.status@1",
|
||||
"org.tribes.trust.attest@1"
|
||||
]
|
||||
|
||||
def federation_capabilities, do: @federation_capabilities
|
||||
|
||||
def federation_hidden? do
|
||||
case Tribes.ConfigStore.get_for_plugin("trust", "federation.hidden", default: false) do
|
||||
{:ok, value} -> value in [true, "true", "1", "on"]
|
||||
{:error, _reason} -> false
|
||||
end
|
||||
end
|
||||
|
||||
def set_federation_hidden?(value, opts \\ []) when is_boolean(value) do
|
||||
Tribes.ConfigStore.put_for_plugin("trust", "federation.hidden", value, opts)
|
||||
end
|
||||
|
||||
def receive_hello(attrs, opts \\ []) when is_map(attrs) do
|
||||
with {:ok, tribe_attrs} <- normalize_remote_tribe_attrs(attrs),
|
||||
{:ok, relationship_attrs} <- normalize_inbound_relationship_attrs(attrs),
|
||||
{:ok, tribe} <- Domain.upsert_remote_tribe(tribe_attrs, default_trust_opts(opts)),
|
||||
{:ok, relationship} <-
|
||||
upsert_inbound_relationship(tribe.pubkey, relationship_attrs, opts) do
|
||||
{:ok, %{tribe: tribe, relationship: relationship}}
|
||||
end
|
||||
end
|
||||
|
||||
def observe_remote_tribe(attrs, opts \\ []) when is_map(attrs) do
|
||||
with {:ok, tribe_attrs} <- normalize_remote_tribe_attrs(attrs),
|
||||
{:ok, relationship_attrs} <- normalize_observed_relationship_attrs(attrs),
|
||||
{:ok, tribe} <- Domain.upsert_remote_tribe(tribe_attrs, default_trust_opts(opts)),
|
||||
{:ok, relationship} <-
|
||||
ensure_observed_relationship(tribe.pubkey, relationship_attrs, opts) do
|
||||
{:ok, %{tribe: tribe, relationship: relationship}}
|
||||
end
|
||||
end
|
||||
|
||||
def list_relationship_rows(opts \\ []) do
|
||||
with {:ok, tribes} <- Domain.list_remote_tribes(default_trust_opts(opts)),
|
||||
{:ok, relationships} <- Domain.list_tribe_relationships(default_trust_opts(opts)) do
|
||||
tribes_by_pubkey = Map.new(tribes, &{&1.pubkey, &1})
|
||||
|
||||
rows =
|
||||
relationships
|
||||
|> Enum.map(fn relationship ->
|
||||
%{
|
||||
tribe: Map.get(tribes_by_pubkey, relationship.remote_tribe_pubkey),
|
||||
relationship: relationship
|
||||
}
|
||||
end)
|
||||
|> Enum.sort_by(fn %{relationship: relationship} ->
|
||||
{status_rank(relationship.status), relationship.updated_at || relationship.inserted_at}
|
||||
end)
|
||||
|
||||
{:ok, rows}
|
||||
end
|
||||
end
|
||||
|
||||
def update_relationship(remote_pubkey, attrs, opts \\ []) when is_binary(remote_pubkey) do
|
||||
with {:ok, pubkey} <- normalize_pubkey(remote_pubkey),
|
||||
{:ok, existing} <-
|
||||
Domain.get_tribe_relationship(
|
||||
pubkey,
|
||||
Keyword.put(default_trust_opts(opts), :not_found_error?, false)
|
||||
),
|
||||
{:ok, attrs} <- normalize_relationship_update(attrs, existing) do
|
||||
Domain.upsert_tribe_relationship(attrs, default_trust_opts(opts))
|
||||
end
|
||||
end
|
||||
|
||||
defp upsert_inbound_relationship(remote_pubkey, attrs, opts) do
|
||||
existing =
|
||||
case Domain.get_tribe_relationship(
|
||||
remote_pubkey,
|
||||
Keyword.put(default_trust_opts(opts), :not_found_error?, false)
|
||||
) do
|
||||
{:ok, relationship} -> relationship
|
||||
{:error, _reason} -> nil
|
||||
end
|
||||
|
||||
status =
|
||||
case existing do
|
||||
%{status: status} when status in [:active, :blocked, :revoked] -> status
|
||||
_other -> :pending_inbound
|
||||
end
|
||||
|
||||
attrs
|
||||
|> Map.merge(%{remote_tribe_pubkey: remote_pubkey, status: status})
|
||||
|> Domain.upsert_tribe_relationship(default_trust_opts(opts))
|
||||
end
|
||||
|
||||
defp ensure_observed_relationship(remote_pubkey, attrs, opts) do
|
||||
case Domain.get_tribe_relationship(
|
||||
remote_pubkey,
|
||||
Keyword.put(default_trust_opts(opts), :not_found_error?, false)
|
||||
) do
|
||||
{:ok, nil} ->
|
||||
Domain.upsert_tribe_relationship(
|
||||
Map.merge(
|
||||
%{
|
||||
remote_tribe_pubkey: remote_pubkey,
|
||||
status: :observed,
|
||||
requested_capabilities: [],
|
||||
granted_capabilities: [],
|
||||
trust_score: 0
|
||||
},
|
||||
attrs
|
||||
),
|
||||
default_trust_opts(opts)
|
||||
)
|
||||
|
||||
{:ok, relationship} ->
|
||||
{:ok, relationship}
|
||||
|
||||
{:error, _reason} = error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_remote_tribe_attrs(attrs) do
|
||||
with {:ok, pubkey} <-
|
||||
normalize_pubkey(attrs["from_tribe"] || attrs["pubkey"] || attrs[:pubkey]) do
|
||||
{:ok,
|
||||
%{
|
||||
pubkey: pubkey,
|
||||
name: normalize_optional_string(attrs["name"] || attrs[:name]) || pubkey,
|
||||
description: normalize_optional_string(attrs["description"] || attrs[:description]),
|
||||
last_seen_at: DateTime.utc_now()
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_inbound_relationship_attrs(attrs) do
|
||||
with {:ok, requested_capabilities} <-
|
||||
normalize_string_list(
|
||||
attrs["requested_capabilities"] || attrs[:requested_capabilities] || []
|
||||
),
|
||||
{:ok, remote_relay_urls} <-
|
||||
normalize_string_list(attrs["relay_urls"] || attrs[:relay_urls] || []),
|
||||
{:ok, remote_capabilities} <-
|
||||
normalize_string_list(attrs["capabilities"] || attrs[:capabilities] || []) do
|
||||
{:ok,
|
||||
%{
|
||||
remote_api_url: normalize_optional_string(attrs["api_url"] || attrs[:api_url]),
|
||||
remote_profile_url:
|
||||
normalize_optional_string(attrs["profile_url"] || attrs[:profile_url]),
|
||||
remote_relay_urls: remote_relay_urls,
|
||||
remote_capabilities: remote_capabilities,
|
||||
requested_capabilities: requested_capabilities,
|
||||
inbound_message: normalize_optional_string(attrs["message"] || attrs[:message]),
|
||||
last_inbound_at: DateTime.utc_now()
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_observed_relationship_attrs(attrs) do
|
||||
with {:ok, remote_relay_urls} <-
|
||||
normalize_string_list(attrs["relay_urls"] || attrs[:relay_urls] || []),
|
||||
{:ok, remote_capabilities} <-
|
||||
normalize_string_list(attrs["capabilities"] || attrs[:capabilities] || []) do
|
||||
{:ok,
|
||||
%{
|
||||
remote_api_url: normalize_optional_string(attrs["api_url"] || attrs[:api_url]),
|
||||
remote_profile_url:
|
||||
normalize_optional_string(attrs["profile_url"] || attrs[:profile_url]),
|
||||
remote_relay_urls: remote_relay_urls,
|
||||
remote_capabilities: remote_capabilities
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_relationship_update(_attrs, nil), do: {:error, :relationship_not_found}
|
||||
|
||||
defp normalize_relationship_update(attrs, existing) do
|
||||
with {:ok, status} <- normalize_status(attrs["status"] || attrs[:status] || existing.status),
|
||||
{:ok, trust_score} <-
|
||||
normalize_trust_score(
|
||||
attrs["trust_score"] || attrs[:trust_score] || existing.trust_score
|
||||
),
|
||||
{:ok, granted_capabilities} <-
|
||||
normalize_capability_input(
|
||||
attrs["granted_capabilities"] || attrs[:granted_capabilities] ||
|
||||
existing.granted_capabilities || []
|
||||
) do
|
||||
{:ok,
|
||||
%{
|
||||
remote_tribe_pubkey: existing.remote_tribe_pubkey,
|
||||
status: status,
|
||||
remote_api_url: existing.remote_api_url,
|
||||
remote_profile_url: existing.remote_profile_url,
|
||||
remote_relay_urls: existing.remote_relay_urls || [],
|
||||
remote_capabilities: existing.remote_capabilities || [],
|
||||
requested_capabilities: existing.requested_capabilities || [],
|
||||
granted_capabilities: granted_capabilities,
|
||||
trust_score: trust_score,
|
||||
trust_note:
|
||||
normalize_optional_string(
|
||||
attrs["trust_note"] || attrs[:trust_note] || existing.trust_note
|
||||
),
|
||||
inbound_message: existing.inbound_message,
|
||||
last_inbound_at: existing.last_inbound_at,
|
||||
last_outbound_at: existing.last_outbound_at
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_capability_input(value) when is_binary(value) do
|
||||
value
|
||||
|> String.split([",", "\n"], trim: true)
|
||||
|> Enum.map(&String.trim/1)
|
||||
|> normalize_string_list()
|
||||
end
|
||||
|
||||
defp normalize_capability_input(value), do: normalize_string_list(value)
|
||||
|
||||
defp normalize_pubkey(value) when is_binary(value) do
|
||||
pubkey = value |> String.trim() |> String.downcase()
|
||||
|
||||
if String.match?(pubkey, ~r/\A[0-9a-f]{64}\z/) do
|
||||
{:ok, pubkey}
|
||||
else
|
||||
{:error, :invalid_pubkey}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_pubkey(_value), do: {:error, :invalid_pubkey}
|
||||
|
||||
defp normalize_status(:observed), do: {:ok, :observed}
|
||||
defp normalize_status(:pending_inbound), do: {:ok, :pending_inbound}
|
||||
defp normalize_status(:pending_outbound), do: {:ok, :pending_outbound}
|
||||
defp normalize_status(:active), do: {:ok, :active}
|
||||
defp normalize_status(:blocked), do: {:ok, :blocked}
|
||||
defp normalize_status(:revoked), do: {:ok, :revoked}
|
||||
defp normalize_status("observed"), do: {:ok, :observed}
|
||||
defp normalize_status("pending_inbound"), do: {:ok, :pending_inbound}
|
||||
defp normalize_status("pending_outbound"), do: {:ok, :pending_outbound}
|
||||
defp normalize_status("active"), do: {:ok, :active}
|
||||
defp normalize_status("blocked"), do: {:ok, :blocked}
|
||||
defp normalize_status("revoked"), do: {:ok, :revoked}
|
||||
defp normalize_status(_status), do: {:error, :invalid_status}
|
||||
|
||||
defp normalize_trust_score(value) when is_integer(value) and value >= -100 and value <= 100,
|
||||
do: {:ok, value}
|
||||
|
||||
defp normalize_trust_score(value) when is_binary(value) do
|
||||
case Integer.parse(String.trim(value)) do
|
||||
{score, ""} -> normalize_trust_score(score)
|
||||
_other -> {:error, :invalid_trust_score}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_trust_score(_value), do: {:error, :invalid_trust_score}
|
||||
|
||||
defp normalize_string_list(values) when is_list(values) do
|
||||
values
|
||||
|> Enum.reduce_while({:ok, []}, fn
|
||||
value, {:ok, acc} when is_binary(value) ->
|
||||
trimmed = String.trim(value)
|
||||
next = if trimmed == "", do: acc, else: [trimmed | acc]
|
||||
{:cont, {:ok, next}}
|
||||
|
||||
_value, _acc ->
|
||||
{:halt, {:error, :invalid_string_list}}
|
||||
end)
|
||||
|> case do
|
||||
{:ok, values} -> {:ok, Enum.reverse(values)}
|
||||
{:error, _reason} = error -> error
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_string_list(_values), do: {:error, :invalid_string_list}
|
||||
|
||||
defp normalize_optional_string(nil), do: nil
|
||||
|
||||
defp normalize_optional_string(value) when is_binary(value) do
|
||||
case String.trim(value) do
|
||||
"" -> nil
|
||||
trimmed -> trimmed
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_optional_string(_value), do: nil
|
||||
|
||||
defp status_rank(:pending_inbound), do: 0
|
||||
defp status_rank(:pending_outbound), do: 1
|
||||
defp status_rank(:active), do: 2
|
||||
defp status_rank(:observed), do: 3
|
||||
defp status_rank(:blocked), do: 4
|
||||
defp status_rank(:revoked), do: 5
|
||||
defp status_rank(_status), do: 6
|
||||
|
||||
defp default_trust_opts(opts) do
|
||||
Keyword.put_new(opts, :context, %{private: %{system?: true, system_purpose: :trust_plugin}})
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
defmodule TribeOne.TribesPlugin.Trust.Application do
|
||||
@moduledoc """
|
||||
OTP Application for this plugin.
|
||||
|
||||
Uncomment the `mod:` entry in mix.exs to activate this supervision tree.
|
||||
Add plugin-specific GenServers, workers, or supervisors as children here.
|
||||
"""
|
||||
|
||||
use Application
|
||||
|
||||
@impl true
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
# Add your supervised processes here, e.g.:
|
||||
# {TribeOne.TribesPlugin.Trust.Worker, []}
|
||||
]
|
||||
|
||||
opts = [strategy: :one_for_one, name: TribeOne.TribesPlugin.Trust.Supervisor]
|
||||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
defmodule TribeOne.TribesPlugin.Trust.Domain do
|
||||
@moduledoc false
|
||||
|
||||
use Ash.Domain,
|
||||
otp_app: :tribe_one_trust
|
||||
|
||||
alias TribeOne.TribesPlugin.Trust.RemoteTribe
|
||||
alias TribeOne.TribesPlugin.Trust.TribeRelationship
|
||||
|
||||
resources do
|
||||
resource RemoteTribe do
|
||||
define(:list_remote_tribes, action: :read)
|
||||
define(:get_remote_tribe, action: :by_pubkey, args: [:pubkey])
|
||||
define(:upsert_remote_tribe, action: :upsert)
|
||||
end
|
||||
|
||||
resource TribeRelationship do
|
||||
define(:list_tribe_relationships, action: :read)
|
||||
define(:get_tribe_relationship, action: :by_remote_pubkey, args: [:remote_tribe_pubkey])
|
||||
define(:upsert_tribe_relationship, action: :upsert)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,258 @@
|
||||
defmodule TribeOne.TribesPlugin.Trust.Plugin do
|
||||
@moduledoc """
|
||||
Tribes plugin entry point.
|
||||
"""
|
||||
|
||||
use Tribes.Plugin.Base, otp_app: :tribe_one_trust
|
||||
|
||||
import Plug.Conn
|
||||
import Phoenix.Controller, only: [json: 2]
|
||||
|
||||
alias Parrhesia.API.Auth
|
||||
alias Parrhesia.Web.Listener
|
||||
alias TribeOne.TribesPlugin.Trust
|
||||
|
||||
@impl true
|
||||
def register(context) do
|
||||
super(context)
|
||||
|> Map.merge(%{
|
||||
nav_items: [
|
||||
%{
|
||||
label: "Trust",
|
||||
path: "/trust",
|
||||
icon: nil,
|
||||
requires: [],
|
||||
order: 50
|
||||
}
|
||||
],
|
||||
pages: [
|
||||
%{
|
||||
path: "/trust",
|
||||
live_view: TribeOne.TribesPlugin.TrustWeb.HomeLive,
|
||||
layout: nil
|
||||
}
|
||||
],
|
||||
api_routes: [],
|
||||
management_methods: [],
|
||||
metrics: [],
|
||||
plugs: [],
|
||||
hooks: %{},
|
||||
ash_domains: [TribeOne.TribesPlugin.Trust.Domain],
|
||||
config_schema: %{
|
||||
title: "Trust",
|
||||
description: "Tribe-to-tribe federation and trust settings.",
|
||||
groups: [
|
||||
%{
|
||||
id: "federation",
|
||||
label: "Federation",
|
||||
settings: [
|
||||
%{
|
||||
key: "federation.hidden",
|
||||
label: "Hide unauthenticated federation identity",
|
||||
type: :boolean,
|
||||
default: false,
|
||||
description:
|
||||
"When enabled, /.well-known/tribes/identity returns 404 unless a signed federation call is used."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
def federation_identity(conn) do
|
||||
cond do
|
||||
Trust.federation_hidden?() ->
|
||||
conn |> put_status(:not_found) |> json(%{"ok" => false, "error" => "not-found"})
|
||||
|
||||
true ->
|
||||
case local_identity(api_url(conn)) do
|
||||
{:ok, identity} ->
|
||||
json(conn, identity)
|
||||
|
||||
{:error, :local_tribe_not_found} ->
|
||||
conn |> put_status(404) |> json(%{"ok" => false, "error" => "local-tribe-not-found"})
|
||||
|
||||
{:error, reason} ->
|
||||
conn |> put_status(503) |> json(%{"ok" => false, "error" => inspect(reason)})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def federation_hello(conn) do
|
||||
authorization = get_req_header(conn, "authorization") |> List.first()
|
||||
full_url = full_request_url(conn)
|
||||
|
||||
with {:ok, auth_context} <- Auth.validate_nip98(authorization, conn.method, full_url),
|
||||
{:ok, attrs} <- parse_hello(conn.body_params),
|
||||
:ok <- verify_sender(auth_context.pubkey, attrs),
|
||||
{:ok, result} <- Trust.receive_hello(attrs) do
|
||||
json(conn, %{
|
||||
"ok" => true,
|
||||
"relationship" => %{
|
||||
"remote_tribe_pubkey" => result.relationship.remote_tribe_pubkey,
|
||||
"status" => to_string(result.relationship.status),
|
||||
"trust_score" => result.relationship.trust_score,
|
||||
"granted_capabilities" => result.relationship.granted_capabilities || []
|
||||
}
|
||||
})
|
||||
else
|
||||
{:error, :missing_authorization} ->
|
||||
conn |> put_status(401) |> json(%{"ok" => false, "error" => "auth-required"})
|
||||
|
||||
{:error, :invalid_authorization} ->
|
||||
conn |> put_status(401) |> json(%{"ok" => false, "error" => "invalid-authorization"})
|
||||
|
||||
{:error, :invalid_event} ->
|
||||
conn |> put_status(401) |> json(%{"ok" => false, "error" => "invalid-auth-event"})
|
||||
|
||||
{:error, :stale_event} ->
|
||||
conn |> put_status(401) |> json(%{"ok" => false, "error" => "stale-auth-event"})
|
||||
|
||||
{:error, :replayed_auth_event} ->
|
||||
conn |> put_status(401) |> json(%{"ok" => false, "error" => "replayed-auth-event"})
|
||||
|
||||
{:error, :invalid_method_tag} ->
|
||||
conn |> put_status(401) |> json(%{"ok" => false, "error" => "auth-method-tag-mismatch"})
|
||||
|
||||
{:error, :invalid_url_tag} ->
|
||||
conn |> put_status(401) |> json(%{"ok" => false, "error" => "auth-url-tag-mismatch"})
|
||||
|
||||
{:error, :sender_mismatch} ->
|
||||
conn |> put_status(403) |> json(%{"ok" => false, "error" => "sender-mismatch"})
|
||||
|
||||
{:error, :invalid_payload} ->
|
||||
conn |> put_status(400) |> json(%{"ok" => false, "error" => "invalid-payload"})
|
||||
|
||||
{:error, reason} ->
|
||||
conn |> put_status(400) |> json(%{"ok" => false, "error" => inspect(reason)})
|
||||
end
|
||||
end
|
||||
|
||||
defp local_identity(api_url) when is_binary(api_url) do
|
||||
with {:ok, tribe} when not is_nil(tribe) <- Tribes.Alliance.local_tribe(authorize?: false),
|
||||
{:ok, relay_urls} <- Tribes.Cluster.list_public_relay_urls() do
|
||||
{:ok,
|
||||
%{
|
||||
"protocol" => "org.tribes.federation@1",
|
||||
"tribe" => %{
|
||||
"pubkey" => tribe.pubkey,
|
||||
"name" => tribe.name,
|
||||
"description" => tribe.description,
|
||||
"visibility" => to_string(tribe.visibility)
|
||||
},
|
||||
"api_url" => api_url,
|
||||
"relay_urls" => relay_urls,
|
||||
"capabilities" => Trust.federation_capabilities()
|
||||
}}
|
||||
else
|
||||
{:ok, nil} -> {:error, :local_tribe_not_found}
|
||||
{:error, _reason} = error -> error
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_hello(%{"from_tribe" => from_tribe} = params) when is_binary(from_tribe) do
|
||||
if is_list(Map.get(params, "relay_urls", [])) and is_list(Map.get(params, "capabilities", [])) and
|
||||
is_list(Map.get(params, "requested_capabilities", [])) do
|
||||
{:ok, params}
|
||||
else
|
||||
{:error, :invalid_payload}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_hello(_params), do: {:error, :invalid_payload}
|
||||
|
||||
defp verify_sender(pubkey, %{"from_tribe" => from_tribe}) when is_binary(pubkey) do
|
||||
if String.downcase(pubkey) == String.downcase(from_tribe) do
|
||||
:ok
|
||||
else
|
||||
{:error, :sender_mismatch}
|
||||
end
|
||||
end
|
||||
|
||||
defp verify_sender(_pubkey, _attrs), do: {:error, :sender_mismatch}
|
||||
|
||||
defp api_url(conn) do
|
||||
base_url(conn) <> "/api/tribes/v1"
|
||||
end
|
||||
|
||||
defp full_request_url(conn) do
|
||||
query_suffix = if conn.query_string == "", do: "", else: "?#{conn.query_string}"
|
||||
base_url(conn) <> Listener.visible_request_path(conn) <> query_suffix
|
||||
end
|
||||
|
||||
defp base_url(conn) do
|
||||
scheme = forwarded_scheme(conn)
|
||||
{host, host_port} = forwarded_host(conn)
|
||||
port = forwarded_port(conn, scheme, host_port)
|
||||
|
||||
port_suffix =
|
||||
cond do
|
||||
scheme == :http and port == 80 -> ""
|
||||
scheme == :https and port == 443 -> ""
|
||||
true -> ":#{port}"
|
||||
end
|
||||
|
||||
"#{scheme}://#{host}#{port_suffix}"
|
||||
end
|
||||
|
||||
defp forwarded_scheme(conn) do
|
||||
case first_forwarded_header(conn, "x-forwarded-proto") do
|
||||
"https" -> :https
|
||||
"http" -> :http
|
||||
_other -> conn.scheme
|
||||
end
|
||||
end
|
||||
|
||||
defp forwarded_host(conn) do
|
||||
case first_forwarded_header(conn, "x-forwarded-host") do
|
||||
nil -> {conn.host, nil}
|
||||
forwarded_host -> parse_forwarded_host(forwarded_host)
|
||||
end
|
||||
end
|
||||
|
||||
defp forwarded_port(conn, scheme, host_port) do
|
||||
forwarded? =
|
||||
Enum.any?(["x-forwarded-proto", "x-forwarded-host", "x-forwarded-port"], fn header ->
|
||||
is_binary(first_forwarded_header(conn, header))
|
||||
end)
|
||||
|
||||
case first_forwarded_header(conn, "x-forwarded-port") do
|
||||
nil when is_integer(host_port) -> host_port
|
||||
nil when forwarded? -> default_port(scheme)
|
||||
nil -> conn.port
|
||||
port -> parse_port(port, default_port(scheme))
|
||||
end
|
||||
end
|
||||
|
||||
defp first_forwarded_header(conn, header) do
|
||||
conn
|
||||
|> get_req_header(header)
|
||||
|> List.first()
|
||||
|> case do
|
||||
nil -> nil
|
||||
value -> value |> String.split(",", parts: 2) |> List.first() |> String.trim()
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_forwarded_host(host) when is_binary(host) do
|
||||
case URI.parse("//" <> host) do
|
||||
%URI{host: parsed_host, port: port} when is_binary(parsed_host) and parsed_host != "" ->
|
||||
{parsed_host, port}
|
||||
|
||||
_other ->
|
||||
{host, nil}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_port(value, fallback) when is_binary(value) do
|
||||
case Integer.parse(value) do
|
||||
{port, ""} when port > 0 -> port
|
||||
_other -> fallback
|
||||
end
|
||||
end
|
||||
|
||||
defp default_port(:https), do: 443
|
||||
defp default_port(_scheme), do: 80
|
||||
end
|
||||
@@ -0,0 +1,103 @@
|
||||
defmodule TribeOne.TribesPlugin.Trust.RemoteTribe do
|
||||
@moduledoc false
|
||||
|
||||
use Ash.Resource,
|
||||
otp_app: :tribe_one_trust,
|
||||
domain: TribeOne.TribesPlugin.Trust.Domain,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
authorizers: [Ash.Policy.Authorizer],
|
||||
extensions: [AshNostrSync]
|
||||
|
||||
postgres do
|
||||
table("trust_remote_tribes")
|
||||
repo(Tribes.Repo)
|
||||
|
||||
custom_indexes do
|
||||
index([:pubkey], unique: true)
|
||||
end
|
||||
end
|
||||
|
||||
nostr_sync do
|
||||
namespace("plugins.trust.remote_tribe")
|
||||
lane(:control)
|
||||
publish?(true)
|
||||
consume?(true)
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults([:read])
|
||||
|
||||
read :by_pubkey do
|
||||
get?(true)
|
||||
|
||||
argument :pubkey, :string do
|
||||
allow_nil?(false)
|
||||
end
|
||||
|
||||
filter(expr(pubkey == ^arg(:pubkey)))
|
||||
end
|
||||
|
||||
create :upsert do
|
||||
accept([:pubkey, :name, :description, :last_seen_at])
|
||||
upsert?(true)
|
||||
upsert_identity(:unique_pubkey)
|
||||
change(AshNostrSync.PublishChange)
|
||||
end
|
||||
|
||||
create :sync_upsert do
|
||||
accept([:id, :pubkey, :name, :description, :last_seen_at])
|
||||
upsert?(true)
|
||||
end
|
||||
end
|
||||
|
||||
policies do
|
||||
bypass Tribes.Checks.SyncInteraction do
|
||||
authorize_if(always())
|
||||
end
|
||||
|
||||
bypass {Tribes.Checks.SystemInteraction, purposes: [:trust_plugin]} do
|
||||
authorize_if(always())
|
||||
end
|
||||
|
||||
policy action_type(:read) do
|
||||
authorize_if(Tribes.Checks.ActorLocalTribeAdmin)
|
||||
end
|
||||
|
||||
policy action(:upsert) do
|
||||
authorize_if(Tribes.Checks.ActorLocalTribeAdmin)
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
attribute :id, :uuid do
|
||||
allow_nil?(false)
|
||||
primary_key?(true)
|
||||
public?(true)
|
||||
writable?(true)
|
||||
default(&Ash.UUID.generate/0)
|
||||
end
|
||||
|
||||
attribute :pubkey, :string do
|
||||
allow_nil?(false)
|
||||
public?(true)
|
||||
end
|
||||
|
||||
attribute :name, :string do
|
||||
public?(true)
|
||||
end
|
||||
|
||||
attribute :description, :string do
|
||||
public?(true)
|
||||
end
|
||||
|
||||
attribute :last_seen_at, :utc_datetime do
|
||||
public?(true)
|
||||
end
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
identities do
|
||||
identity(:unique_pubkey, [:pubkey])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,189 @@
|
||||
defmodule TribeOne.TribesPlugin.Trust.TribeRelationship do
|
||||
@moduledoc false
|
||||
|
||||
use Ash.Resource,
|
||||
otp_app: :tribe_one_trust,
|
||||
domain: TribeOne.TribesPlugin.Trust.Domain,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
authorizers: [Ash.Policy.Authorizer],
|
||||
extensions: [AshNostrSync]
|
||||
|
||||
postgres do
|
||||
table("trust_tribe_relationships")
|
||||
repo(Tribes.Repo)
|
||||
|
||||
custom_indexes do
|
||||
index([:remote_tribe_pubkey], unique: true)
|
||||
index([:status])
|
||||
index([:trust_score])
|
||||
end
|
||||
end
|
||||
|
||||
nostr_sync do
|
||||
namespace("plugins.trust.tribe_relationship")
|
||||
lane(:control)
|
||||
publish?(true)
|
||||
consume?(true)
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults([:read])
|
||||
|
||||
read :by_remote_pubkey do
|
||||
get?(true)
|
||||
|
||||
argument :remote_tribe_pubkey, :string do
|
||||
allow_nil?(false)
|
||||
end
|
||||
|
||||
filter(expr(remote_tribe_pubkey == ^arg(:remote_tribe_pubkey)))
|
||||
end
|
||||
|
||||
create :upsert do
|
||||
accept([
|
||||
:remote_tribe_pubkey,
|
||||
:status,
|
||||
:remote_api_url,
|
||||
:remote_profile_url,
|
||||
:remote_relay_urls,
|
||||
:remote_capabilities,
|
||||
:requested_capabilities,
|
||||
:granted_capabilities,
|
||||
:trust_score,
|
||||
:trust_note,
|
||||
:inbound_message,
|
||||
:last_inbound_at,
|
||||
:last_outbound_at
|
||||
])
|
||||
|
||||
upsert?(true)
|
||||
upsert_identity(:unique_remote_tribe_pubkey)
|
||||
change(AshNostrSync.PublishChange)
|
||||
end
|
||||
|
||||
create :sync_upsert do
|
||||
accept([
|
||||
:id,
|
||||
:remote_tribe_pubkey,
|
||||
:status,
|
||||
:remote_api_url,
|
||||
:remote_profile_url,
|
||||
:remote_relay_urls,
|
||||
:remote_capabilities,
|
||||
:requested_capabilities,
|
||||
:granted_capabilities,
|
||||
:trust_score,
|
||||
:trust_note,
|
||||
:inbound_message,
|
||||
:last_inbound_at,
|
||||
:last_outbound_at
|
||||
])
|
||||
|
||||
upsert?(true)
|
||||
end
|
||||
end
|
||||
|
||||
policies do
|
||||
bypass Tribes.Checks.SyncInteraction do
|
||||
authorize_if(always())
|
||||
end
|
||||
|
||||
bypass {Tribes.Checks.SystemInteraction, purposes: [:trust_plugin]} do
|
||||
authorize_if(always())
|
||||
end
|
||||
|
||||
policy action_type(:read) do
|
||||
authorize_if(Tribes.Checks.ActorLocalTribeAdmin)
|
||||
end
|
||||
|
||||
policy action(:upsert) do
|
||||
authorize_if(Tribes.Checks.ActorLocalTribeAdmin)
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
attribute :id, :uuid do
|
||||
allow_nil?(false)
|
||||
primary_key?(true)
|
||||
public?(true)
|
||||
writable?(true)
|
||||
default(&Ash.UUID.generate/0)
|
||||
end
|
||||
|
||||
attribute :remote_tribe_pubkey, :string do
|
||||
allow_nil?(false)
|
||||
public?(true)
|
||||
end
|
||||
|
||||
attribute :status, :atom do
|
||||
constraints(
|
||||
one_of: [:observed, :pending_inbound, :pending_outbound, :active, :blocked, :revoked]
|
||||
)
|
||||
|
||||
allow_nil?(false)
|
||||
default(:observed)
|
||||
public?(true)
|
||||
end
|
||||
|
||||
attribute :remote_api_url, :string do
|
||||
public?(true)
|
||||
end
|
||||
|
||||
attribute :remote_profile_url, :string do
|
||||
public?(true)
|
||||
end
|
||||
|
||||
attribute :remote_relay_urls, {:array, :string} do
|
||||
allow_nil?(false)
|
||||
default([])
|
||||
public?(true)
|
||||
end
|
||||
|
||||
attribute :remote_capabilities, {:array, :string} do
|
||||
allow_nil?(false)
|
||||
default([])
|
||||
public?(true)
|
||||
end
|
||||
|
||||
attribute :requested_capabilities, {:array, :string} do
|
||||
allow_nil?(false)
|
||||
default([])
|
||||
public?(true)
|
||||
end
|
||||
|
||||
attribute :granted_capabilities, {:array, :string} do
|
||||
allow_nil?(false)
|
||||
default([])
|
||||
public?(true)
|
||||
end
|
||||
|
||||
attribute :trust_score, :integer do
|
||||
allow_nil?(false)
|
||||
default(0)
|
||||
constraints(min: -100, max: 100)
|
||||
public?(true)
|
||||
end
|
||||
|
||||
attribute :trust_note, :string do
|
||||
public?(true)
|
||||
end
|
||||
|
||||
attribute :inbound_message, :string do
|
||||
public?(true)
|
||||
end
|
||||
|
||||
attribute :last_inbound_at, :utc_datetime do
|
||||
public?(true)
|
||||
end
|
||||
|
||||
attribute :last_outbound_at, :utc_datetime do
|
||||
public?(true)
|
||||
end
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
identities do
|
||||
identity(:unique_remote_tribe_pubkey, [:remote_tribe_pubkey])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,321 @@
|
||||
defmodule TribeOne.TribesPlugin.TrustWeb.HomeLive do
|
||||
@moduledoc false
|
||||
|
||||
use Phoenix.LiveView
|
||||
|
||||
on_mount({Tribes.Plugin.LiveUserAuth, :local_tribe_admin_required})
|
||||
|
||||
alias TribeOne.TribesPlugin.Trust
|
||||
alias Tribes.Plugin.Layouts
|
||||
|
||||
import Tribes.Plugin.Gettext
|
||||
import Tribes.UI.Components
|
||||
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "Trust")
|
||||
|> assign(:rows, [])
|
||||
|> assign(:federation_hidden?, Trust.federation_hidden?())
|
||||
|> assign(:summary, %{active: 0, pending: 0, passive: 0, blocked: 0})
|
||||
|> assign(:observe_form, observe_form(%{}))
|
||||
|> load_relationships()}
|
||||
end
|
||||
|
||||
def handle_event("validate-observe", %{"tribe" => params}, socket) do
|
||||
{:noreply, assign(socket, :observe_form, observe_form(params))}
|
||||
end
|
||||
|
||||
def handle_event("observe", %{"tribe" => params}, socket) do
|
||||
case Trust.observe_remote_tribe(params) do
|
||||
{:ok, _result} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Remote tribe saved")
|
||||
|> assign(:observe_form, observe_form(%{}))
|
||||
|> load_relationships()}
|
||||
|
||||
{:error, :invalid_pubkey} ->
|
||||
{:noreply, put_flash(socket, :error, "Tribe pubkey must be 64 lowercase hex chars")}
|
||||
|
||||
{:error, reason} ->
|
||||
{:noreply, put_flash(socket, :error, "Failed to save remote tribe: #{inspect(reason)}")}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
"update-relationship",
|
||||
%{"relationship" => %{"remote_tribe_pubkey" => pubkey} = params},
|
||||
socket
|
||||
) do
|
||||
case Trust.update_relationship(pubkey, params) do
|
||||
{:ok, _relationship} ->
|
||||
{:noreply, socket |> put_flash(:info, "Relationship updated") |> load_relationships()}
|
||||
|
||||
{:error, :invalid_trust_score} ->
|
||||
{:noreply, put_flash(socket, :error, "Trust score must be between -100 and 100")}
|
||||
|
||||
{:error, reason} ->
|
||||
{:noreply, put_flash(socket, :error, "Failed to update relationship: #{inspect(reason)}")}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("set-status", %{"pubkey" => pubkey, "status" => status}, socket) do
|
||||
case Trust.update_relationship(pubkey, %{"status" => status}) do
|
||||
{:ok, _relationship} ->
|
||||
{:noreply,
|
||||
socket |> put_flash(:info, "Relationship status updated") |> load_relationships()}
|
||||
|
||||
{:error, reason} ->
|
||||
{:noreply, put_flash(socket, :error, "Failed to update relationship: #{inspect(reason)}")}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("set-hidden", %{"hidden" => hidden}, socket) do
|
||||
hidden? = hidden in ["true", "1", "on"]
|
||||
|
||||
case Trust.set_federation_hidden?(hidden?) do
|
||||
{:ok, _entry} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Federation visibility updated")
|
||||
|> assign(:federation_hidden?, hidden?)}
|
||||
|
||||
{:error, reason} ->
|
||||
{:noreply,
|
||||
put_flash(socket, :error, "Failed to update federation visibility: #{inspect(reason)}")}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("refresh", _params, socket), do: {:noreply, load_relationships(socket)}
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash} current_scope={@current_scope}>
|
||||
<div id="trust-page" class="trust space-y-6 p-6">
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body space-y-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="card-title">Trust</h1>
|
||||
<p class="mt-1 text-sm text-base-content/70">
|
||||
Tribe-to-tribe federation, relationships, and local trust assertions.
|
||||
</p>
|
||||
</div>
|
||||
<button id="trust-refresh" class="btn btn-ghost btn-sm" phx-click="refresh" type="button">
|
||||
{tgettext("Refresh")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-4">
|
||||
<.summary_stat id="trust-active-count" label="Active" value={@summary.active} tone="success" />
|
||||
<.summary_stat id="trust-pending-count" label="Pending" value={@summary.pending} tone="warning" />
|
||||
<.summary_stat id="trust-passive-count" label="Passive" value={@summary.passive} tone="neutral" />
|
||||
<.summary_stat id="trust-blocked-count" label="Blocked" value={@summary.blocked} tone="danger" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body space-y-4">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h2 class="card-title text-base">Federation discovery</h2>
|
||||
<p class="text-sm text-base-content/70">
|
||||
Hidden tribes do not answer unauthenticated well-known identity requests.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button id="trust-visible" class={["btn btn-sm", !@federation_hidden? && "btn-primary", @federation_hidden? && "btn-ghost"]} phx-click="set-hidden" phx-value-hidden="false" type="button">
|
||||
Visible
|
||||
</button>
|
||||
<button id="trust-hidden" class={["btn btn-sm", @federation_hidden? && "btn-primary", !@federation_hidden? && "btn-ghost"]} phx-click="set-hidden" phx-value-hidden="true" type="button">
|
||||
Hidden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body space-y-4">
|
||||
<div>
|
||||
<h2 class="card-title text-base">Observe a remote tribe</h2>
|
||||
<p class="text-sm text-base-content/70">
|
||||
Signed hello requests create pending inbound rows automatically.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<.form for={@observe_form} id="trust-observe-form" phx-change="validate-observe" phx-submit="observe" class="grid gap-3 md:grid-cols-2">
|
||||
<.input field={@observe_form[:pubkey]} type="text" label="Tribe pubkey" required />
|
||||
<.input field={@observe_form[:name]} type="text" label="Name" />
|
||||
<.input field={@observe_form[:api_url]} type="url" label="API URL" placeholder="https://example.org/api/tribes/v1" />
|
||||
<.input field={@observe_form[:profile_url]} type="url" label="Profile URL" />
|
||||
<div class="md:col-span-2 flex justify-end">
|
||||
<button id="trust-observe-submit" type="submit" class="btn btn-primary btn-sm">Save remote tribe</button>
|
||||
</div>
|
||||
</.form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-base">Known remote tribes</h2>
|
||||
|
||||
<div id="trust-relationships-list" class="mt-3 space-y-3">
|
||||
<%= for row <- @rows do %>
|
||||
<div id={"relationship-row-#{row.relationship.remote_tribe_pubkey}"} class="rounded-box border border-base-300 p-4">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div class="min-w-0 flex-1 space-y-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h3 class="font-semibold">{remote_name(row)}</h3>
|
||||
<span class={status_badge_class(row.relationship.status)}>{status_label(row.relationship.status)}</span>
|
||||
<span class={trust_badge_class(row.relationship.trust_score)}>trust {row.relationship.trust_score}</span>
|
||||
</div>
|
||||
<p class="break-all font-mono text-xs text-base-content/70">{row.relationship.remote_tribe_pubkey}</p>
|
||||
<p :if={row.tribe && row.tribe.description} class="text-sm text-base-content/70">{row.tribe.description}</p>
|
||||
<p :if={row.relationship.remote_api_url} class="break-all text-xs text-base-content/70">API: {row.relationship.remote_api_url}</p>
|
||||
<p :if={row.relationship.inbound_message} class="rounded-box bg-base-200 p-2 text-sm text-base-content/80">{row.relationship.inbound_message}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2 lg:justify-end">
|
||||
<button :if={row.relationship.status in [:observed, :pending_inbound, :pending_outbound, :revoked]} id={"activate-relationship-#{row.relationship.remote_tribe_pubkey}"} class="btn btn-success btn-xs" phx-click="set-status" phx-value-pubkey={row.relationship.remote_tribe_pubkey} phx-value-status="active" type="button">Activate</button>
|
||||
<button :if={row.relationship.status != :blocked} id={"block-relationship-#{row.relationship.remote_tribe_pubkey}"} class="btn btn-ghost btn-xs text-error" phx-click="set-status" phx-value-pubkey={row.relationship.remote_tribe_pubkey} phx-value-status="blocked" type="button">Block</button>
|
||||
<button :if={row.relationship.status in [:active, :blocked]} id={"revoke-relationship-#{row.relationship.remote_tribe_pubkey}"} class="btn btn-ghost btn-xs" phx-click="set-status" phx-value-pubkey={row.relationship.remote_tribe_pubkey} phx-value-status="revoked" type="button">Revoke</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<.form for={relationship_form(row.relationship)} id={"relationship-form-#{row.relationship.remote_tribe_pubkey}"} phx-submit="update-relationship" class="mt-4 grid gap-3 md:grid-cols-3">
|
||||
<.input field={relationship_form(row.relationship)[:remote_tribe_pubkey]} type="hidden" />
|
||||
<.input field={relationship_form(row.relationship)[:status]} type="select" label="Status" options={status_options()} />
|
||||
<.input field={relationship_form(row.relationship)[:trust_score]} type="number" label="Trust score" min="-100" max="100" />
|
||||
<.input field={relationship_form(row.relationship)[:granted_capabilities]} type="text" label="Granted capabilities" placeholder="org.tribes.kobold.dataset.discover@1" />
|
||||
<div class="md:col-span-3">
|
||||
<.input field={relationship_form(row.relationship)[:trust_note]} type="textarea" label="Trust note" rows="2" />
|
||||
</div>
|
||||
<div class="md:col-span-3 flex justify-end">
|
||||
<button id={"save-relationship-#{row.relationship.remote_tribe_pubkey}"} type="submit" class="btn btn-primary btn-xs">Save trust</button>
|
||||
</div>
|
||||
</.form>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<p :if={@rows == []} class="text-sm text-base-content/70">
|
||||
No remote tribes have contacted this tribe yet.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
attr(:id, :string, required: true)
|
||||
attr(:label, :string, required: true)
|
||||
attr(:value, :integer, required: true)
|
||||
attr(:tone, :string, default: "neutral")
|
||||
|
||||
defp summary_stat(assigns) do
|
||||
~H"""
|
||||
<div id={@id} class="rounded-box border border-base-300 bg-base-200 p-3">
|
||||
<p class="text-xs font-medium uppercase tracking-wide text-base-content/60">{@label}</p>
|
||||
<p class={summary_value_class(@tone)}>{@value}</p>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp load_relationships(socket) do
|
||||
case Trust.list_relationship_rows() do
|
||||
{:ok, rows} ->
|
||||
socket
|
||||
|> assign(:rows, rows)
|
||||
|> assign(:federation_hidden?, Trust.federation_hidden?())
|
||||
|> assign(:summary, summarize(rows))
|
||||
|
||||
{:error, reason} ->
|
||||
put_flash(socket, :error, "Failed to load trust relationships: #{inspect(reason)}")
|
||||
end
|
||||
end
|
||||
|
||||
defp summarize(rows) do
|
||||
Enum.reduce(rows, %{active: 0, pending: 0, passive: 0, blocked: 0}, fn row, acc ->
|
||||
case row.relationship.status do
|
||||
:active ->
|
||||
Map.update!(acc, :active, &(&1 + 1))
|
||||
|
||||
status when status in [:pending_inbound, :pending_outbound] ->
|
||||
Map.update!(acc, :pending, &(&1 + 1))
|
||||
|
||||
:blocked ->
|
||||
Map.update!(acc, :blocked, &(&1 + 1))
|
||||
|
||||
_status ->
|
||||
Map.update!(acc, :passive, &(&1 + 1))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp observe_form(params), do: to_form(params, as: :tribe)
|
||||
|
||||
defp relationship_form(relationship) do
|
||||
to_form(
|
||||
%{
|
||||
"remote_tribe_pubkey" => relationship.remote_tribe_pubkey,
|
||||
"status" => to_string(relationship.status),
|
||||
"trust_score" => relationship.trust_score,
|
||||
"granted_capabilities" => Enum.join(relationship.granted_capabilities || [], ", "),
|
||||
"trust_note" => relationship.trust_note || ""
|
||||
},
|
||||
as: :relationship
|
||||
)
|
||||
end
|
||||
|
||||
defp remote_name(%{tribe: %{name: name}}) when is_binary(name) and name != "", do: name
|
||||
defp remote_name(_row), do: "Unnamed tribe"
|
||||
|
||||
defp status_options do
|
||||
[
|
||||
{"Observed", "observed"},
|
||||
{"Pending inbound", "pending_inbound"},
|
||||
{"Pending outbound", "pending_outbound"},
|
||||
{"Active", "active"},
|
||||
{"Blocked", "blocked"},
|
||||
{"Revoked", "revoked"}
|
||||
]
|
||||
end
|
||||
|
||||
defp status_label(:observed), do: "observed"
|
||||
defp status_label(:pending_inbound), do: "pending inbound"
|
||||
defp status_label(:pending_outbound), do: "pending outbound"
|
||||
defp status_label(:active), do: "active"
|
||||
defp status_label(:blocked), do: "blocked"
|
||||
defp status_label(:revoked), do: "revoked"
|
||||
defp status_label(_status), do: "unknown"
|
||||
|
||||
defp status_badge_class(:active),
|
||||
do: "rounded-full bg-emerald-100 px-2 py-0.5 text-[11px] font-medium text-emerald-700"
|
||||
|
||||
defp status_badge_class(status) when status in [:pending_inbound, :pending_outbound],
|
||||
do: "rounded-full bg-amber-100 px-2 py-0.5 text-[11px] font-medium text-amber-700"
|
||||
|
||||
defp status_badge_class(:blocked),
|
||||
do: "rounded-full bg-rose-100 px-2 py-0.5 text-[11px] font-medium text-rose-700"
|
||||
|
||||
defp status_badge_class(_status),
|
||||
do: "rounded-full bg-base-300 px-2 py-0.5 text-[11px] font-medium text-base-content/70"
|
||||
|
||||
defp trust_badge_class(score) when is_integer(score) and score >= 60,
|
||||
do: "rounded-full bg-emerald-100 px-2 py-0.5 text-[11px] font-medium text-emerald-700"
|
||||
|
||||
defp trust_badge_class(score) when is_integer(score) and score <= -60,
|
||||
do: "rounded-full bg-rose-100 px-2 py-0.5 text-[11px] font-medium text-rose-700"
|
||||
|
||||
defp trust_badge_class(_score),
|
||||
do: "rounded-full bg-base-300 px-2 py-0.5 text-[11px] font-medium text-base-content/70"
|
||||
|
||||
defp summary_value_class("success"), do: "mt-1 text-2xl font-semibold text-emerald-700"
|
||||
defp summary_value_class("warning"), do: "mt-1 text-2xl font-semibold text-amber-700"
|
||||
defp summary_value_class("danger"), do: "mt-1 text-2xl font-semibold text-rose-700"
|
||||
defp summary_value_class(_tone), do: "mt-1 text-2xl font-semibold text-base-content"
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"id": "org.tribe-one.plugins.trust",
|
||||
"slug": "trust",
|
||||
"display_name": "Trust",
|
||||
"version": "0.1.0",
|
||||
"description": "Tribe-to-tribe alliances and trust",
|
||||
"entry_module": "TribeOne.TribesPlugin.Trust.Plugin",
|
||||
"host_api": "1",
|
||||
"otp_app": "tribe_one_trust",
|
||||
"provides": ["org.tribes.alliance.trust@1", "org.tribes.federation.provider@1"],
|
||||
"requires": ["org.tribe-one.caps.ui@1"],
|
||||
"enhances_with": [],
|
||||
"assets": {
|
||||
"global_js": ["trust.js"],
|
||||
"global_css": ["trust.css"]
|
||||
},
|
||||
"migrations": true,
|
||||
"children": false
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
defmodule TribeOne.TribesPlugin.Trust.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :tribe_one_trust,
|
||||
version: "0.1.0",
|
||||
elixir: "~> 1.18",
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps(),
|
||||
aliases: aliases(),
|
||||
usage_rules: usage_rules()
|
||||
]
|
||||
end
|
||||
|
||||
def cli do
|
||||
[
|
||||
preferred_envs: [precommit: :test, raw_precommit: :test, raw_test: :test]
|
||||
]
|
||||
end
|
||||
|
||||
def application do
|
||||
[
|
||||
extra_applications: [:logger]
|
||||
# Uncomment if your plugin needs its own supervision tree:
|
||||
# mod: {TribeOne.TribesPlugin.Trust.Application, []}
|
||||
]
|
||||
end
|
||||
|
||||
defp elixirc_paths(:test), do: ["lib", "test/support"]
|
||||
defp elixirc_paths(_), do: ["lib"]
|
||||
|
||||
defp deps do
|
||||
[
|
||||
# Plugin API dependency for local development alongside a tribes checkout.
|
||||
#
|
||||
# For CI or standalone development, this can be replaced with a published
|
||||
# package once tribes_plugin_api is released.
|
||||
{:tribes_plugin_api, path: "../tribes/tribes_plugin_api", runtime: false},
|
||||
{:tribes_plugin, path: "../tribes-plugin-new", only: [:dev, :test], runtime: false},
|
||||
{:igniter, "~> 0.7", only: [:dev, :test], runtime: false},
|
||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||
{:lazy_html, ">= 0.1.0", only: :test},
|
||||
{:phoenix, "~> 1.8"},
|
||||
{:phoenix_html, "~> 4.1"},
|
||||
{:phoenix_live_view, "~> 1.1.0"},
|
||||
{:usage_rules, "~> 1.2", only: :dev}
|
||||
] ++ tribes_deps(Mix.env())
|
||||
end
|
||||
|
||||
defp tribes_deps(:dev), do: [{:tribes, path: "../tribes", only: :dev, runtime: false}]
|
||||
defp tribes_deps(:test), do: [{:tribes, path: "../tribes", only: :test}]
|
||||
defp tribes_deps(_), do: []
|
||||
|
||||
defp usage_rules do
|
||||
[
|
||||
file: "AGENTS.md",
|
||||
usage_rules: [
|
||||
{:usage_rules, sub_rules: []},
|
||||
{"usage_rules:elixir", main: false},
|
||||
{"usage_rules:otp", main: false},
|
||||
"phoenix:ecto",
|
||||
"phoenix:html",
|
||||
"phoenix:liveview",
|
||||
"phoenix:phoenix",
|
||||
{:ash, sub_rules: []},
|
||||
{"ash:actions", link: :markdown, main: false},
|
||||
{"ash:migrations", link: :markdown, main: false},
|
||||
{"ash:testing", link: :markdown, main: false},
|
||||
{:tribes_plugin_api, sub_rules: []},
|
||||
{:tribes, sub_rules: []}
|
||||
]
|
||||
]
|
||||
end
|
||||
|
||||
defp aliases do
|
||||
[
|
||||
test: &plugin_test_message/1,
|
||||
raw_test: &raw_test/1,
|
||||
lint: ["format --check-formatted", "credo"],
|
||||
precommit: &plugin_precommit_message/1,
|
||||
raw_precommit: &raw_precommit/1
|
||||
]
|
||||
end
|
||||
|
||||
defp plugin_test_message(_args) do
|
||||
Mix.raise("""
|
||||
This plugin test suite is host-backed. Use `plugin test` or `scripts/plugin test`.
|
||||
|
||||
For low-level debugging only, set up the host environment yourself and run `mix raw_test`.
|
||||
""")
|
||||
end
|
||||
|
||||
defp plugin_precommit_message(_args) do
|
||||
Mix.raise("""
|
||||
This plugin precommit is host-backed. Use `plugin precommit` or `scripts/plugin precommit`.
|
||||
|
||||
For low-level debugging only, set up the host environment yourself and run `mix raw_precommit`.
|
||||
""")
|
||||
end
|
||||
|
||||
defp raw_precommit(args) do
|
||||
Mix.Task.run("format")
|
||||
Mix.Task.run("compile", ["--warnings-as-errors"])
|
||||
Mix.Task.run("credo", ["--strict", "--all"])
|
||||
Mix.Task.run("deps.unlock", ["--unused"])
|
||||
raw_test(args)
|
||||
end
|
||||
|
||||
defp raw_test(args), do: Mix.Tasks.Test.run(args)
|
||||
end
|
||||
@@ -0,0 +1,104 @@
|
||||
%{
|
||||
"absinthe": {:hex, :absinthe, "1.10.2", "7951efe6d22c7524752dc5e3fd4289e5bfb1d1e9ee2da2c79431c585552ded2e", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3948d6948c45b5cfd375892e578943eac8642d0a34b15e2a92ffdcdda9d91a22"},
|
||||
"absinthe_phoenix": {:hex, :absinthe_phoenix, "2.0.5", "1fdcfc59db241b1d5e45e4c8c9db162277e64ec5728e0fd6958a3fa491a05b82", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.5", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.5", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.13 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "086c6d4a1c32f7444713130d204c87b1b006169f5159026b73f02f7d38ccd05c"},
|
||||
"absinthe_plug": {:hex, :absinthe_plug, "1.5.10", "c9e207235aaa8a086a5db6801a9bebaea035f7b5a2703cb98d962646ef70c76f", [:mix], [{:absinthe, "~> 1.7", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "489ac1951c8e4128571141c60a0669a720619bc161f801a8c6be8cfaf7ab0979"},
|
||||
"argon2_elixir": {:hex, :argon2_elixir, "4.1.3", "4f28318286f89453364d7fbb53e03d4563fd7ed2438a60237eba5e426e97785f", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "7c295b8d8e0eaf6f43641698f962526cdf87c6feb7d14bd21e599271b510608c"},
|
||||
"ash": {:hex, :ash, "3.27.2", "5c69bf6c3f53bf46329af8689fdb85c620c42284d03e17d9dd7aca32a2468daf", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 1.0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d7ac246e396fa1d22b9fdcd5e82914cf7763817e21664f87736a0f78fe5782e6"},
|
||||
"ash_authentication": {:hex, :ash_authentication, "4.13.7", "421b5ddb516026f6794435980a632109ec116af2afa68a45e15fb48b41c92cfa", [:mix], [{:argon2_elixir, "~> 4.0", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:ash, "~> 3.7", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_postgres, ">= 2.6.8 and < 3.0.0-0", [hex: :ash_postgres, repo: "hexpm", optional: true]}, {:assent, "> 0.2.0 and < 0.3.0", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:finch, "~> 0.19", [hex: :finch, repo: "hexpm", optional: false]}, {:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}], "hexpm", "0d45ac3fdcca6902dabbe161ce63e9cea8f90583863c2e14261c9309e5837121"},
|
||||
"ash_authentication_phoenix": {:hex, :ash_authentication_phoenix, "2.16.0", "02045ecde9eeb30ab1bfffbdf693c64426af24902bcd533765eba725b9b9f46f", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_authentication, "~> 4.10", [hex: :ash_authentication, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.3.11 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:igniter, ">= 0.5.25 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:slugify, "~> 1.3", [hex: :slugify, repo: "hexpm", optional: false]}], "hexpm", "1107a45af771ee7c02ebe82abcaf9a778096e66b3e6cb2b6e614d22d1fe385f7"},
|
||||
"ash_graphql": {:hex, :ash_graphql, "1.9.4", "3e75eaa0917e1085c938aadfec8205cb028a161842d13b9304475f56a8b1c6a0", [:mix], [{:absinthe, "~> 1.7", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_phoenix, "~> 2.0", [hex: :absinthe_phoenix, repo: "hexpm", optional: true]}, {:absinthe_plug, "~> 1.4", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:ash, ">= 3.5.13 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:igniter, ">= 0.5.28 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:spark, ">= 2.2.10", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "da3a7b3026ca31b006bf675f6dcbe49c4575c42cad794684373fc7a51893165b"},
|
||||
"ash_phoenix": {:hex, :ash_phoenix, "2.3.22", "f59a347ee09e1fa9973fe1b2faf7f8f793acc14dc09341062783b8eb1a9f5c99", [:mix], [{:ash, ">= 3.5.13 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:inertia, "~> 2.3", [hex: :inertia, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.6 or ~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.3 or ~> 1.0-rc.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:spark, ">= 2.2.29 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "4b34ea84e122c238ad1843888b8fd4d21aec27605b9b1e6e27e1b70329560fbb"},
|
||||
"ash_postgres": {:hex, :ash_postgres, "2.9.1", "bf4229d65706f794650edb47c9f30138a6e2d5af6efe002ca38e619306cca9f6", [:mix], [{:ash, "~> 3.24", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, "~> 0.6", [hex: :ash_sql, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.4 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "72c0366649985a858d4ef8f906968cee339dfd7519bb0beaa2b4d87f3d5b0bb9"},
|
||||
"ash_sql": {:hex, :ash_sql, "0.6.3", "a708b34ba71b40141dab9e75dc44a095885ae4635b25135d3fd4c3620b299b97", [:mix], [{:ash, ">= 3.24.5 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, ">= 3.13.4 and < 4.0.0-0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "3ee461380d96dca32766a210ea60c64783f690ad5565f0434a00cd475e71e8b9"},
|
||||
"assent": {:hex, :assent, "0.2.13", "11226365d2d8661d23e9a2cf94d3255e81054ff9d88ac877f28bfdf38fa4ef31", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "bf9f351b01dd6bceea1d1f157f05438f6765ce606e6eb8d29296003d29bf6eab"},
|
||||
"bandit": {:hex, :bandit, "1.11.1", "1eb33123cc3c17ae0c3447874eb83399ee530f960c39711ed240342fbd4865fa", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d4401016df9abbc6dcd325c0b78b2b193e7c7c96bb68f31e576112be025d84a5"},
|
||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"},
|
||||
"bech32": {:hex, :bech32, "1.0.0", "85a3bb58c408d735b5becb39e8a23536660ec0df1ef0afee72377c130939de1b", [:mix], [], "hexpm", "f781b8524c30a524922613d97c1858c27bd9f639b4e6b350f4a4843ee97607d3"},
|
||||
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||
"castore": {:hex, :castore, "1.0.19", "6903cabdfd9d1af46454126e7c8385186659dd33ecfb74a885cae52221ad6109", [:mix], [], "hexpm", "3669e6cab13f54c2df26b3e6833745d647f35b6e30d8ddd5975df0d5c842ca98"},
|
||||
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
|
||||
"cldr_utils": {:hex, :cldr_utils, "2.29.7", "2189bc0117efe91c684558e79174b45eb43135595b7d1fe9b57f53917be195c1", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "4bddcd597fee34e2d2829ae9ef62bcfef8d97ae5f6b75f0c6ee37a3db31aa73a"},
|
||||
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
|
||||
"cowboy": {:hex, :cowboy, "2.15.0", "9cfe86ed7117bf045e10adbedb0170af7be57f2a3637e7be143433d8dd267396", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "179fb65140fb440a17b767ad53b755081506f9596c4db5c49c0396d8c8643668"},
|
||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
||||
"cowlib": {:hex, :cowlib, "2.16.1", "318d385d55f657e9a5005838c4e426e13dcd724a691438384b6165a69687e531", [:make, :rebar3], [], "hexpm", "58f1e425a9e04176f1d30e20116f57c4e90ef0e187552e9741c465bdf4044f70"},
|
||||
"credo": {:hex, :credo, "1.7.18", "5c5596bf7aedf9c8c227f13272ac499fe8eae6237bd326f2f07dfc173786f042", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "a189d164685fd945809e862fe76a7420c4398fa288d76257662aecb909d6b3e5"},
|
||||
"crux": {:hex, :crux, "0.1.3", "c698dee09d811678dcddad11a02a832c6bff100f1a7aee49ac44c87485bdbac8", [:mix], [{:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "04188ea9c1cee13e3ef132417200765857402dcc581f45a8a7862eec3b0530ff"},
|
||||
"db_connection": {:hex, :db_connection, "2.10.1", "d5465f6bcc125c1b8981c1dbf23c193ca16f446ec0b25832dc174f74f18be510", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "18ed94c6e627b4bf452dbd4df61b69a35a1e768525140bc1917b7a685026a6a3"},
|
||||
"decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"},
|
||||
"digital_token": {:hex, :digital_token, "1.0.0", "454a4444061943f7349a51ef74b7fb1ebd19e6a94f43ef711f7dae88c09347df", [:mix], [{:cldr_utils, "~> 2.17", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "8ed6f5a8c2fa7b07147b9963db506a1b4c7475d9afca6492136535b064c9e9e6"},
|
||||
"dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
|
||||
"ecto": {:hex, :ecto, "3.14.0", "2fa64521eebfcb2670d907a86e4ad947290e9933706bb315e6fb5c21b172cb26", [:mix], [{:decimal, "~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "130d69ffb4285f9ce4792b65dfbb994fd13ea4cbc3cbea2524b199aa3de84af3"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.14.0", "06446ab8410d2f85bfbb80857ee224ab3b693700cbb38f6535d507449a627b2e", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.8", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f4d8d36faf294c9417b5a37ec7ac8217ee2abdef5fcf197ba690f361548d3949"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
|
||||
"esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"},
|
||||
"ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"},
|
||||
"ex_ast": {:hex, :ex_ast, "0.12.0", "052ad63711da41b7efbfb3490dbf3d757bb67caec17d02f6deb0db4a0363e5f6", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.7", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "66b4797f157d32f0a63c6da227515f78816c0ac8f621f6d7a2b22108e7b4dd85"},
|
||||
"ex_cldr": {:hex, :ex_cldr, "2.47.4", "64e1efda73d6c5fa0342d5b6554f39b740fa02ed540dc03997facf573101ad9e", [:mix], [{:cldr_utils, "~> 2.29", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "918aabc032955f3eac70abbdf2c5469433132edfaaaccee55451f074ee1ccdba"},
|
||||
"ex_cldr_calendars": {:hex, :ex_cldr_calendars, "2.4.3", "13ec2ab7e1738ec16ecb91c0edc3753e9925c2682f4184571368d183b41deb7e", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: true]}, {:ex_cldr_numbers, "~> 2.36", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_cldr_units, "~> 3.20", [hex: :ex_cldr_units, repo: "hexpm", optional: true]}, {:ex_doc, "~> 0.21", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b46ef6bd74f7e2dc3de27366f79372b1e630563bcf09b7803fec162e28d4a85e"},
|
||||
"ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.17.2", "4892b5255a205e8133297b702abbe13c9b51d0281fa8c2ea100f51df9657c3c7", [:mix], [{:ex_cldr, "~> 2.38", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "797095c106a2fe6632981531e29cfb1d2f8ee7de626f4d6243f974d6f74a0112"},
|
||||
"ex_cldr_dates_times": {:hex, :ex_cldr_dates_times, "2.25.6", "6db974ab2b430b5733994c2bfbe98a69e25eeb076b876a929791ff521f8fdd96", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:ex_cldr_calendars, "~> 2.4", [hex: :ex_cldr_calendars, repo: "hexpm", optional: false]}, {:ex_cldr_units, "~> 3.20", [hex: :ex_cldr_units, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:tz, "~> 0.26", [hex: :tz, repo: "hexpm", optional: true]}], "hexpm", "926ff5662b849f86088832ee66b61a96aab0fa5a54d5e14240e08ad3030663e2"},
|
||||
"ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.38.2", "7fa47c9547aa5e6e14e952fa0f69e464a6e6328f0ceda84668b88efec4c5a3ae", [:mix], [{:decimal, "~> 1.6 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.45", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.17", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "a7eda0b3c7b8700a290e4330c6e0f36239225b987ee0332ace19c364ed3a3618"},
|
||||
"expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"},
|
||||
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
|
||||
"finch": {:hex, :finch, "0.22.0", "5c48fa6f9706a78eb9036cacb67b8b996b4e66d111c543f4c29bb0f879a6806b", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b94e83c47780fc6813f746a1f1a34ee65cda42da4c5ea26a68f0acc4498e23dc"},
|
||||
"fine": {:hex, :fine, "0.1.6", "4bf7151493443c454aac9f2fa2f34f5fefd0346a83fb5586a016c4a135c63247", [:mix], [], "hexpm", "5638eb4495488e885ebec167fa57973e5c35e1a50c344eb7666c90ec1c4e3b12"},
|
||||
"gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"},
|
||||
"glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"},
|
||||
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
|
||||
"idna": {:hex, :idna, "7.1.0", "1067a13043538129602d2f2ce6899d8713125c7d19734aa557ce2e3ea55bd4f1", [:rebar3], [], "hexpm", "6ae959a025bf36df61a8cab8508d9654891b5426a84c44d82deaffd6ddf8c71f"},
|
||||
"igniter": {:hex, :igniter, "0.8.0", "c7cab589440e5f20ff68e00f60eb094378114dab3105c0784ce8140f8dfdd2c0", [:mix], [{:ex_ast, "~> 0.5", [hex: :ex_ast, repo: "hexpm", optional: false]}, {:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "fcd99096fde4797f7b48bebddcfc58785569acd696346a3eb385bf813f47a7cc"},
|
||||
"iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"},
|
||||
"jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"},
|
||||
"joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"},
|
||||
"jose": {:hex, :jose, "1.11.12", "06e62b467b61d3726cbc19e9b5489f7549c37993de846dfb3ee8259f9ed208b3", [:mix, :rebar3], [], "hexpm", "31e92b653e9210b696765cdd885437457de1add2a9011d92f8cf63e4641bab7b"},
|
||||
"lazy_html": {:hex, :lazy_html, "0.1.11", "136c8e9cd616b4f4e9c1562daa683880891120b759606dc4c3b6b18058ba5d79", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "3b1be592929c31eca1a21673d25696e5c14cddfe922d9d1a3e3b48be4163883b"},
|
||||
"lib_secp256k1": {:hex, :lib_secp256k1, "0.7.1", "53cad778b8da3a29e453a7a477517d99fb5f13f615c8050eb2db8fd1dce7a1db", [:make, :mix], [{:elixir_make, "~> 0.9", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "78bdd3661a17448aff5aeec5ca74c8ddbc09b01f0ecfa3ba1aba3e8ae47ab2b3"},
|
||||
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
||||
"mint": {:hex, :mint, "1.8.0", "b964eaf4416f2dee2ba88968d52239fca5621b0402b9c95f55a08eb9d74803e9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "f3c572c11355eccf00f22275e9b42463bc17bd28db13be1e28f8e0bb4adbc849"},
|
||||
"multigraph": {:hex, :multigraph, "0.16.1-mg.4", "2bbe149f5411b0e3bf0624c7bf2e3da2738efeac2f9a67bbbcb807ab171f0a76", [:mix], [], "hexpm", "b9f3e2577cef4658eeedf97c76d22a86d33a7aab702a93c1da9c122e849e9037"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
||||
"owl": {:hex, :owl, "0.13.0", "26010e066d5992774268f3163506972ddac0a7e77bfe57fa42a250f24d6b876e", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "59bf9d11ce37a4db98f57cb68fbfd61593bf419ec4ed302852b6683d3d2f7475"},
|
||||
"parrhesia": {:hex, :parrhesia, "0.14.0", "7ff96a6d645740a19b66567c3346162735ba821a90377616e74493637c35195e", [:mix], [{:bandit, "~> 1.5", [hex: :bandit, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:lib_secp256k1, "~> 0.7", [hex: :lib_secp256k1, repo: "hexpm", optional: false]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus, "~> 1.1", [hex: :telemetry_metrics_prometheus, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5", [hex: :websock_adapter, repo: "hexpm", optional: false]}, {:websockex, "~> 0.4", [hex: :websockex, repo: "hexpm", optional: false]}], "hexpm", "2bda91e6f385b6990465f2ccc9e8e0266105a31f72e2b466ae247f846ec3ef28"},
|
||||
"phoenix": {:hex, :phoenix, "1.8.7", "d8d755b4ff4b449f610223dd706b4ae64155cb720d3dc09c706c079ecea189e4", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "47352f72d6ab31009ef77516b1b3a14745be97b54061fd458031b9d8294869d5"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
|
||||
"phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"},
|
||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "1.1.30", "a84af1610755dc208da35d4d45564485edbf18c3f3c77373c4a650dc994cdcdb", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a353c51ac1e3190910f01a6100c7d5cc02c5e22e7374fd817bd3aedd21149039"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"},
|
||||
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
|
||||
"phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
|
||||
"picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"},
|
||||
"plug": {:hex, :plug, "1.19.2", "e4950525b22c6789dfb38a3f95d47171ba159da3fc5a33be9643b43d5e8adb98", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b6fce20a56af5e60fa5dfecf3f907bb98ec981be43c79a3809a499bc3d133de0"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.8.1", "5aa391a5e8d1ac3192e36a3bcaff12b5fd6ef6c7e29b53a38e63a860783e77d0", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "4c200288673d5bc86a0ab7dc6a2a069176a74e5d573ef62740a1c517458a5f26"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
|
||||
"postgrex": {:hex, :postgrex, "0.22.2", "4aec14df2a72722aee92492566edbeeb44e233ecb86b1915d03136297ef1385d", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8946382ddb06294f56026ac4278b3cc212bac8a2c82ed68b4087819ed1abc53b"},
|
||||
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
|
||||
"reactor": {:hex, :reactor, "1.0.2", "79e4e81d016ab0016afd10bb4c18cb3a574f08f10f8e53be5f08ce27f8eed541", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:multigraph, "~> 0.16.1-mg.2", [hex: :multigraph, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.3 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "19fd55aaaadaae28f55133351051c25d4ac217f99e3e5a67940cc4a321e3948e"},
|
||||
"req": {:hex, :req, "0.5.18", "48e6431cb4135e8a7815e745177485369a9b4a9924d5fe68ca00eb09ceaed1ef", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.21.0 or ~> 0.22.0", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "fa03812c440a9754bf34355e0c5d4f3ed316458db62e3284b7a352ef8dc0b996"},
|
||||
"rewrite": {:hex, :rewrite, "1.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"},
|
||||
"slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"},
|
||||
"sourceror": {:hex, :sourceror, "1.12.0", "da354c5f35aad3cc1132f5d5b0d8437d865e2661c263260480bab51b5eedb437", [:mix], [], "hexpm", "755703683bd014ebcd5de9acc24b68fb874a660a568d1d63f8f98cd8a6ef9cd0"},
|
||||
"spark": {:hex, :spark, "2.7.0", "e685b33c038f12851993880bb7e3b326117612eb746fe15828678c152f8321c6", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "e2f675fbda32375b01d9ee7c652671531027fd043bf4a91bafdb2ab716aa1122"},
|
||||
"spitfire": {:hex, :spitfire, "0.3.12", "0f7780e4c6ea3753b65ea0c4924f3dfd5c21a51aaa734ffb9dd0b68d2544f27e", [:mix], [], "hexpm", "a389931287b85330c0e954ab06447e198516ab368a232a0200ed77ca13ca9acf"},
|
||||
"splode": {:hex, :splode, "0.3.1", "9843c54f84f71b7833fec3f0be06c3cfb5be6b35960ee195ea4fad84b1c25030", [:mix], [], "hexpm", "8f2309b6ec2ecbb01435656429ed1d9ed04ba28797a3280c3b0d1217018ecfbd"},
|
||||
"stream_data": {:hex, :stream_data, "1.3.0", "bde37905530aff386dea1ddd86ecbf00e6642dc074ceffc10b7d4e41dfd6aac9", [:mix], [], "hexpm", "3cc552e286e817dca43c98044c706eec9318083a1480c52ae2688b08e2936e3c"},
|
||||
"swoosh": {:hex, :swoosh, "1.25.3", "c181e353138807e49cc761619466a4b2219714018d8af2e409f67786dfb132fa", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, ">= 1.9.0 and < 5.0.0", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, ">= 6.0.0 and < 8.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc5d2cd7f24a3ee4249e02a60a0a5c2519d396a8b8798e1ec44ea78714d68d85"},
|
||||
"telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"},
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
|
||||
"telemetry_metrics_prometheus": {:hex, :telemetry_metrics_prometheus, "1.1.0", "1cc23e932c1ef9aa3b91db257ead31ea58d53229d407e059b29bb962c1505a13", [:mix], [{:plug_cowboy, "~> 2.1", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}], "hexpm", "d43b3659b3244da44fe0275b717701542365d4519b79d9ce895b9719c1ce4d26"},
|
||||
"telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.1", "c9755987d7b959b557084e6990990cb96a50d6482c683fb9622a63837f3cd3d8", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5e2c599da4983c4f88a33e9571f1458bf98b0cf6ba930f1dc3a6e8cf45d5afb6"},
|
||||
"telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"},
|
||||
"text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"},
|
||||
"thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
|
||||
"usage_rules": {:hex, :usage_rules, "1.2.6", "a7b3f8d6e5d265701139d5714749c37c54bb82230a4c51ec54a12a1e4769b9d1", [:mix], [{:igniter, ">= 0.6.6 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "608411b9876a16a9d62a427dbaf42faf458e4cd0a508b3bd7e5ee71502073582"},
|
||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},
|
||||
"websockex": {:hex, :websockex, "0.5.1", "9de28d37bbe34f371eb46e29b79c94c94fff79f93c960d842fbf447253558eb4", [:mix], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8ef39576ed56bc3804c9cd8626f8b5d6b5721848d2726c0ccd4f05385a3c9f14"},
|
||||
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
|
||||
"yaml_elixir": {:hex, :yaml_elixir, "2.12.1", "d74f2d82294651b58dac849c45a82aaea639766797359baff834b64439f6b3f4", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "d9ac16563c737d55f9bfeed7627489156b91268a3a21cd55c54eb2e335207fed"},
|
||||
"ymlr": {:hex, :ymlr, "5.1.5", "0b9207c7940be3f2bc29b77cd55109d5aa2f4dcde6575942017335769e6f5628", [:mix], [], "hexpm", "7030cb240c46850caeb3b01be745307632be319b15f03083136f6251f49b516d"},
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
defmodule Tribes.Repo.Migrations.CreateTrustResourcesExtensions1 do
|
||||
@moduledoc """
|
||||
Installs any extensions that are mentioned in the repo's `installed_extensions/0` callback
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
execute("""
|
||||
CREATE OR REPLACE FUNCTION ash_elixir_or(left BOOLEAN, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE)
|
||||
AS $$ SELECT COALESCE(NULLIF($1, FALSE), $2) $$
|
||||
LANGUAGE SQL
|
||||
SET search_path = ''
|
||||
IMMUTABLE;
|
||||
""")
|
||||
|
||||
execute("""
|
||||
CREATE OR REPLACE FUNCTION ash_elixir_or(left ANYCOMPATIBLE, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE)
|
||||
AS $$ SELECT COALESCE($1, $2) $$
|
||||
LANGUAGE SQL
|
||||
SET search_path = ''
|
||||
IMMUTABLE;
|
||||
""")
|
||||
|
||||
execute("""
|
||||
CREATE OR REPLACE FUNCTION ash_elixir_and(left BOOLEAN, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) AS $$
|
||||
SELECT CASE
|
||||
WHEN $1 IS TRUE THEN $2
|
||||
ELSE $1
|
||||
END $$
|
||||
LANGUAGE SQL
|
||||
SET search_path = ''
|
||||
IMMUTABLE;
|
||||
""")
|
||||
|
||||
execute("""
|
||||
CREATE OR REPLACE FUNCTION ash_elixir_and(left ANYCOMPATIBLE, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) AS $$
|
||||
SELECT CASE
|
||||
WHEN $1 IS NOT NULL THEN $2
|
||||
ELSE $1
|
||||
END $$
|
||||
LANGUAGE SQL
|
||||
SET search_path = ''
|
||||
IMMUTABLE;
|
||||
""")
|
||||
|
||||
execute("""
|
||||
CREATE OR REPLACE FUNCTION ash_trim_whitespace(arr text[])
|
||||
RETURNS text[] AS $$
|
||||
DECLARE
|
||||
start_index INT = 1;
|
||||
end_index INT = array_length(arr, 1);
|
||||
BEGIN
|
||||
WHILE start_index <= end_index AND arr[start_index] = '' LOOP
|
||||
start_index := start_index + 1;
|
||||
END LOOP;
|
||||
|
||||
WHILE end_index >= start_index AND arr[end_index] = '' LOOP
|
||||
end_index := end_index - 1;
|
||||
END LOOP;
|
||||
|
||||
IF start_index > end_index THEN
|
||||
RETURN ARRAY[]::text[];
|
||||
ELSE
|
||||
RETURN arr[start_index : end_index];
|
||||
END IF;
|
||||
END; $$
|
||||
LANGUAGE plpgsql
|
||||
SET search_path = ''
|
||||
IMMUTABLE;
|
||||
""")
|
||||
|
||||
execute("""
|
||||
CREATE OR REPLACE FUNCTION ash_raise_error(json_data jsonb)
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
-- Raise an error with the provided JSON data.
|
||||
-- The JSON object is converted to text for inclusion in the error message.
|
||||
RAISE EXCEPTION 'ash_error: %', json_data::text;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql
|
||||
STABLE
|
||||
SET search_path = '';
|
||||
""")
|
||||
|
||||
execute("""
|
||||
CREATE OR REPLACE FUNCTION ash_raise_error(json_data jsonb, type_signal ANYCOMPATIBLE)
|
||||
RETURNS ANYCOMPATIBLE AS $$
|
||||
BEGIN
|
||||
-- Raise an error with the provided JSON data.
|
||||
-- The JSON object is converted to text for inclusion in the error message.
|
||||
RAISE EXCEPTION 'ash_error: %', json_data::text;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql
|
||||
STABLE
|
||||
SET search_path = '';
|
||||
""")
|
||||
|
||||
execute("""
|
||||
CREATE OR REPLACE FUNCTION ash_required(value ANYCOMPATIBLE, payload jsonb)
|
||||
RETURNS ANYCOMPATIBLE AS $$
|
||||
BEGIN
|
||||
IF value IS NULL THEN
|
||||
RETURN ash_raise_error(payload, value);
|
||||
END IF;
|
||||
|
||||
RETURN value;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql
|
||||
STABLE
|
||||
SET search_path = '';
|
||||
""")
|
||||
|
||||
execute("""
|
||||
CREATE OR REPLACE FUNCTION uuid_generate_v7()
|
||||
RETURNS UUID
|
||||
AS $$
|
||||
DECLARE
|
||||
timestamp TIMESTAMPTZ;
|
||||
microseconds INT;
|
||||
BEGIN
|
||||
timestamp = clock_timestamp();
|
||||
microseconds = (cast(extract(microseconds FROM timestamp)::INT - (floor(extract(milliseconds FROM timestamp))::INT * 1000) AS DOUBLE PRECISION) * 4.096)::INT;
|
||||
|
||||
RETURN encode(
|
||||
set_byte(
|
||||
set_byte(
|
||||
overlay(uuid_send(gen_random_uuid()) placing substring(int8send(floor(extract(epoch FROM timestamp) * 1000)::BIGINT) FROM 3) FROM 1 FOR 6
|
||||
),
|
||||
6, (b'0111' || (microseconds >> 8)::bit(4))::bit(8)::int
|
||||
),
|
||||
7, microseconds::bit(8)::int
|
||||
),
|
||||
'hex')::UUID;
|
||||
END
|
||||
$$
|
||||
LANGUAGE PLPGSQL
|
||||
SET search_path = ''
|
||||
VOLATILE;
|
||||
""")
|
||||
|
||||
execute("CREATE EXTENSION IF NOT EXISTS \"citext\"")
|
||||
end
|
||||
|
||||
def down do
|
||||
# Uncomment this if you actually want to uninstall the extensions
|
||||
# when this migration is rolled back:
|
||||
execute(
|
||||
"DROP FUNCTION IF EXISTS uuid_generate_v7(), ash_raise_error(jsonb), ash_raise_error(jsonb, ANYCOMPATIBLE), ash_elixir_and(BOOLEAN, ANYCOMPATIBLE), ash_elixir_and(ANYCOMPATIBLE, ANYCOMPATIBLE), ash_elixir_or(ANYCOMPATIBLE, ANYCOMPATIBLE), ash_elixir_or(BOOLEAN, ANYCOMPATIBLE), ash_trim_whitespace(text[]), ash_required(ANYCOMPATIBLE, jsonb)"
|
||||
)
|
||||
|
||||
# execute("DROP EXTENSION IF EXISTS \"citext\"")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,113 @@
|
||||
defmodule Tribes.Repo.Migrations.CreateTrustResources do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
create table(:trust_tribe_relationships, primary_key: false) do
|
||||
add(:id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true)
|
||||
add(:remote_tribe_pubkey, :text, null: false)
|
||||
add(:status, :text, null: false, default: "observed")
|
||||
add(:remote_api_url, :text)
|
||||
add(:remote_profile_url, :text)
|
||||
add(:remote_relay_urls, {:array, :text}, null: false, default: [])
|
||||
add(:remote_capabilities, {:array, :text}, null: false, default: [])
|
||||
add(:requested_capabilities, {:array, :text}, null: false, default: [])
|
||||
add(:granted_capabilities, {:array, :text}, null: false, default: [])
|
||||
add(:trust_score, :bigint, null: false, default: 0)
|
||||
add(:trust_note, :text)
|
||||
add(:inbound_message, :text)
|
||||
add(:last_inbound_at, :utc_datetime)
|
||||
add(:last_outbound_at, :utc_datetime)
|
||||
|
||||
add(:inserted_at, :utc_datetime,
|
||||
null: false,
|
||||
default: fragment("(now() AT TIME ZONE 'utc')")
|
||||
)
|
||||
|
||||
add(:updated_at, :utc_datetime,
|
||||
null: false,
|
||||
default: fragment("(now() AT TIME ZONE 'utc')")
|
||||
)
|
||||
|
||||
add(:deleted_at, :utc_datetime)
|
||||
end
|
||||
|
||||
create(index(:trust_tribe_relationships, [:trust_score], where: "deleted_at IS NULL"))
|
||||
|
||||
create(index(:trust_tribe_relationships, [:status], where: "deleted_at IS NULL"))
|
||||
|
||||
create(
|
||||
index(:trust_tribe_relationships, [:remote_tribe_pubkey],
|
||||
unique: true,
|
||||
where: "deleted_at IS NULL"
|
||||
)
|
||||
)
|
||||
|
||||
create(
|
||||
unique_index(:trust_tribe_relationships, [:remote_tribe_pubkey],
|
||||
where: "(deleted_at IS NULL)",
|
||||
name: "trust_tribe_relationships_unique_remote_tribe_pubkey_index"
|
||||
)
|
||||
)
|
||||
|
||||
create table(:trust_remote_tribes, primary_key: false) do
|
||||
add(:id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true)
|
||||
add(:pubkey, :text, null: false)
|
||||
add(:name, :text)
|
||||
add(:description, :text)
|
||||
add(:last_seen_at, :utc_datetime)
|
||||
|
||||
add(:inserted_at, :utc_datetime,
|
||||
null: false,
|
||||
default: fragment("(now() AT TIME ZONE 'utc')")
|
||||
)
|
||||
|
||||
add(:updated_at, :utc_datetime,
|
||||
null: false,
|
||||
default: fragment("(now() AT TIME ZONE 'utc')")
|
||||
)
|
||||
|
||||
add(:deleted_at, :utc_datetime)
|
||||
end
|
||||
|
||||
create(index(:trust_remote_tribes, [:pubkey], unique: true, where: "deleted_at IS NULL"))
|
||||
|
||||
create(
|
||||
unique_index(:trust_remote_tribes, [:pubkey],
|
||||
where: "(deleted_at IS NULL)",
|
||||
name: "trust_remote_tribes_unique_pubkey_index"
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def down do
|
||||
drop_if_exists(
|
||||
unique_index(:trust_remote_tribes, [:pubkey],
|
||||
name: "trust_remote_tribes_unique_pubkey_index"
|
||||
)
|
||||
)
|
||||
|
||||
drop_if_exists(index(:trust_remote_tribes, [:pubkey]))
|
||||
|
||||
drop(table(:trust_remote_tribes))
|
||||
|
||||
drop_if_exists(
|
||||
unique_index(:trust_tribe_relationships, [:remote_tribe_pubkey],
|
||||
name: "trust_tribe_relationships_unique_remote_tribe_pubkey_index"
|
||||
)
|
||||
)
|
||||
|
||||
drop_if_exists(index(:trust_tribe_relationships, [:remote_tribe_pubkey]))
|
||||
|
||||
drop_if_exists(index(:trust_tribe_relationships, [:status]))
|
||||
|
||||
drop_if_exists(index(:trust_tribe_relationships, [:trust_score]))
|
||||
|
||||
drop(table(:trust_tribe_relationships))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"ash_functions_version": 6,
|
||||
"installed": [
|
||||
"ash-functions",
|
||||
"citext"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "pubkey",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "description",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "last_seen_at",
|
||||
"type": "utc_datetime"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "inserted_at",
|
||||
"type": "utc_datetime"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "updated_at",
|
||||
"type": "utc_datetime"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "deleted_at",
|
||||
"type": "utc_datetime"
|
||||
}
|
||||
],
|
||||
"base_filter": "deleted_at IS NULL",
|
||||
"check_constraints": [],
|
||||
"create_table_options": null,
|
||||
"custom_indexes": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"concurrently": false,
|
||||
"error_fields": [
|
||||
"pubkey"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "pubkey"
|
||||
}
|
||||
],
|
||||
"include": null,
|
||||
"message": null,
|
||||
"name": null,
|
||||
"nulls_distinct": true,
|
||||
"prefix": null,
|
||||
"table": null,
|
||||
"unique": true,
|
||||
"using": null,
|
||||
"where": null
|
||||
}
|
||||
],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "C71DB333F342D884DC0BE14D42FE6B95AE4E56E082AAAF7596F48F63A9989696",
|
||||
"identities": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"base_filter": "deleted_at IS NULL",
|
||||
"index_name": "trust_remote_tribes_unique_pubkey_index",
|
||||
"keys": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "pubkey"
|
||||
}
|
||||
],
|
||||
"name": "unique_pubkey",
|
||||
"nils_distinct?": true,
|
||||
"where": null
|
||||
}
|
||||
],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.Tribes.Repo",
|
||||
"schema": null,
|
||||
"table": "trust_remote_tribes"
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "remote_tribe_pubkey",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "\"observed\"",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "status",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "remote_api_url",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "remote_profile_url",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "[]",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "remote_relay_urls",
|
||||
"type": [
|
||||
"array",
|
||||
"text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "[]",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "remote_capabilities",
|
||||
"type": [
|
||||
"array",
|
||||
"text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "[]",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "requested_capabilities",
|
||||
"type": [
|
||||
"array",
|
||||
"text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "[]",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "granted_capabilities",
|
||||
"type": [
|
||||
"array",
|
||||
"text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "0",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "trust_score",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "trust_note",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "inbound_message",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "last_inbound_at",
|
||||
"type": "utc_datetime"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "last_outbound_at",
|
||||
"type": "utc_datetime"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "inserted_at",
|
||||
"type": "utc_datetime"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "updated_at",
|
||||
"type": "utc_datetime"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "deleted_at",
|
||||
"type": "utc_datetime"
|
||||
}
|
||||
],
|
||||
"base_filter": "deleted_at IS NULL",
|
||||
"check_constraints": [],
|
||||
"create_table_options": null,
|
||||
"custom_indexes": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"concurrently": false,
|
||||
"error_fields": [
|
||||
"remote_tribe_pubkey"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "remote_tribe_pubkey"
|
||||
}
|
||||
],
|
||||
"include": null,
|
||||
"message": null,
|
||||
"name": null,
|
||||
"nulls_distinct": true,
|
||||
"prefix": null,
|
||||
"table": null,
|
||||
"unique": true,
|
||||
"using": null,
|
||||
"where": null
|
||||
},
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"concurrently": false,
|
||||
"error_fields": [
|
||||
"status"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "status"
|
||||
}
|
||||
],
|
||||
"include": null,
|
||||
"message": null,
|
||||
"name": null,
|
||||
"nulls_distinct": true,
|
||||
"prefix": null,
|
||||
"table": null,
|
||||
"unique": false,
|
||||
"using": null,
|
||||
"where": null
|
||||
},
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"concurrently": false,
|
||||
"error_fields": [
|
||||
"trust_score"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "trust_score"
|
||||
}
|
||||
],
|
||||
"include": null,
|
||||
"message": null,
|
||||
"name": null,
|
||||
"nulls_distinct": true,
|
||||
"prefix": null,
|
||||
"table": null,
|
||||
"unique": false,
|
||||
"using": null,
|
||||
"where": null
|
||||
}
|
||||
],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "8F49418D629617A5E8D55FF6B3EC495A26E444F98016ECDCEC865A55D43CAEA4",
|
||||
"identities": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"base_filter": "deleted_at IS NULL",
|
||||
"index_name": "trust_tribe_relationships_unique_remote_tribe_pubkey_index",
|
||||
"keys": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "remote_tribe_pubkey"
|
||||
}
|
||||
],
|
||||
"name": "unique_remote_tribe_pubkey",
|
||||
"nils_distinct?": true,
|
||||
"where": null
|
||||
}
|
||||
],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.Tribes.Repo",
|
||||
"schema": null,
|
||||
"table": "trust_tribe_relationships"
|
||||
}
|
||||
Executable
+106
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
plugin validate
|
||||
plugin test [mix test args...]
|
||||
plugin precommit [mix precommit args...]
|
||||
plugin ecto.migration <name> [mix ecto.gen.migration args...]
|
||||
plugin ash.codegen <name> [mix ash_postgres.generate_migrations args...]
|
||||
plugin smoke
|
||||
plugin shell
|
||||
EOF
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "plugin: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
plugin_root="$(cd "$script_dir/.." && pwd -P)"
|
||||
host_root="${TRIBES_HOST_ROOT:-$plugin_root/../tribes}"
|
||||
host_root="$(cd "$host_root" && pwd -P)"
|
||||
host_script="$host_root/scripts/plugin"
|
||||
|
||||
json_field() {
|
||||
local field="$1"
|
||||
|
||||
FIELD="$field" mix run --no-start -e '
|
||||
field = System.fetch_env!("FIELD")
|
||||
manifest = "manifest.json" |> File.read!() |> JSON.decode!()
|
||||
value = Map.fetch!(manifest, field)
|
||||
IO.write(value)
|
||||
'
|
||||
}
|
||||
|
||||
run_smoke() {
|
||||
cd "$plugin_root"
|
||||
|
||||
mix compile
|
||||
|
||||
local otp_app
|
||||
local entry_module
|
||||
otp_app="$(json_field otp_app)"
|
||||
entry_module="$(json_field entry_module)"
|
||||
|
||||
local beam_path="_build/dev/lib/$otp_app/ebin/Elixir.$entry_module.beam"
|
||||
[[ -f "$beam_path" ]] || fail "expected entry module beam at $beam_path"
|
||||
|
||||
cd "$host_root"
|
||||
PLUGIN_ROOT="$plugin_root" ENTRY_MODULE="$entry_module" mix run --no-start -e '
|
||||
plugin_root = System.fetch_env!("PLUGIN_ROOT")
|
||||
entry_module = System.fetch_env!("ENTRY_MODULE")
|
||||
|
||||
plugin_root
|
||||
|> Path.join("_build/dev/lib/*/ebin")
|
||||
|> Path.wildcard()
|
||||
|> Enum.each(&(:code.add_patha(String.to_charlist(&1))))
|
||||
|
||||
module = String.to_atom("Elixir." <> entry_module)
|
||||
|
||||
case Code.ensure_loaded(module) do
|
||||
{:module, ^module} ->
|
||||
IO.puts("Loaded #{entry_module}")
|
||||
|
||||
other ->
|
||||
raise "failed to load #{entry_module}: #{inspect(other)}"
|
||||
end
|
||||
'
|
||||
}
|
||||
|
||||
command_name="${1:-}"
|
||||
if [[ -z "$command_name" ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
|
||||
case "$command_name" in
|
||||
validate | test | precommit | ecto.migration | ash.codegen | smoke | shell) ;;
|
||||
-h | --help | help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
fail "unknown command: $command_name"
|
||||
;;
|
||||
esac
|
||||
|
||||
[[ -x "$host_script" || -f "$host_script" ]] || fail "expected host plugin script at $host_script"
|
||||
|
||||
if [[ "$command_name" == "smoke" ]]; then
|
||||
run_smoke "$@"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${DEVENV_ROOT:-}" == "$plugin_root" ]]; then
|
||||
command -v devenv >/dev/null 2>&1 || fail "devenv is required when running from the plugin devenv shell"
|
||||
cd "$host_root"
|
||||
exec devenv shell -- bash ./scripts/plugin "$command_name" "$plugin_root" "$@"
|
||||
fi
|
||||
|
||||
exec bash "$host_script" "$command_name" "$plugin_root" "$@"
|
||||
@@ -0,0 +1 @@
|
||||
ExUnit.start()
|
||||
@@ -0,0 +1,18 @@
|
||||
defmodule TribeOne.TribesPlugin.Trust.HomePageTest do
|
||||
use Tribes.PluginTest.PageCase, plugin: TribeOne.TribesPlugin.Trust.Plugin
|
||||
|
||||
test "renders the trust page for signed-in tribe admins", %{signed_in_conn: conn} do
|
||||
{:ok, view, html} = live(conn, "/trust")
|
||||
|
||||
assert html =~ "Trust"
|
||||
assert has_element?(view, "#plugin-nav-trust", "Trust")
|
||||
assert has_element?(view, "#trust-page")
|
||||
assert has_element?(view, "#trust-observe-form")
|
||||
end
|
||||
|
||||
test "dispatches top-level subpaths to the plugin page", %{signed_in_conn: conn} do
|
||||
{:ok, _view, html} = live(conn, "/trust/example")
|
||||
|
||||
assert html =~ "Trust"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
defmodule TribeOne.TribesPlugin.Trust.PluginContractTest do
|
||||
use Tribes.PluginTest.ContractTest, plugin: TribeOne.TribesPlugin.Trust.Plugin
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
defmodule TribeOne.TribesPlugin.TrustTest do
|
||||
use Tribes.PluginTest.PageCase, plugin: TribeOne.TribesPlugin.Trust.Plugin
|
||||
|
||||
alias TribeOne.TribesPlugin.Trust
|
||||
|
||||
test "receive_hello stores a pending inbound relationship" do
|
||||
pubkey = String.duplicate("a", 64)
|
||||
|
||||
assert {:ok, result} =
|
||||
Trust.receive_hello(%{
|
||||
"from_tribe" => pubkey,
|
||||
"name" => "Remote Seeds",
|
||||
"api_url" => "https://remote.example/api/tribes/v1",
|
||||
"relay_urls" => ["wss://remote.example/relay"],
|
||||
"capabilities" => ["org.tribes.kobold.dataset.discover@1"],
|
||||
"requested_capabilities" => ["org.tribes.kobold.dataset.sync@1"],
|
||||
"message" => "Can we mirror your catalog?"
|
||||
})
|
||||
|
||||
assert result.tribe.pubkey == pubkey
|
||||
assert result.tribe.name == "Remote Seeds"
|
||||
assert result.relationship.remote_tribe_pubkey == pubkey
|
||||
assert result.relationship.status == :pending_inbound
|
||||
assert result.relationship.remote_api_url == "https://remote.example/api/tribes/v1"
|
||||
assert result.relationship.remote_relay_urls == ["wss://remote.example/relay"]
|
||||
assert result.relationship.requested_capabilities == ["org.tribes.kobold.dataset.sync@1"]
|
||||
end
|
||||
|
||||
test "relationship trust score is bounded" do
|
||||
pubkey = String.duplicate("b", 64)
|
||||
|
||||
assert {:ok, _result} = Trust.receive_hello(%{"from_tribe" => pubkey})
|
||||
|
||||
assert {:error, :invalid_trust_score} =
|
||||
Trust.update_relationship(pubkey, %{"trust_score" => "101"})
|
||||
|
||||
assert {:ok, relationship} = Trust.update_relationship(pubkey, %{"trust_score" => "60"})
|
||||
assert relationship.trust_score == 60
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user