Showing preview only (310K chars total). Download the full file or copy to clipboard to get everything.
Repository: ItalyPaleAle/hereditas
Branch: master
Commit: 097f9d2b9d91
Files: 120
Total size: 280.5 KB
Directory structure:
gitextract_d_jnmmz2/
├── .eslintignore
├── .eslintrc.js
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ ├── docs-ci.yaml
│ ├── docs-production.yaml
│ └── docs-staging.yaml
├── .gitignore
├── .npmignore
├── .vscode/
│ └── settings.json
├── CODE_OF_CONDUCT.md
├── LICENSE.md
├── README.md
├── app/
│ ├── components/
│ │ ├── NavBar.svelte
│ │ ├── PassphraseBox.svelte
│ │ ├── RequestAuthentication.svelte
│ │ └── UserProfile.svelte
│ ├── layout/
│ │ └── App.svelte
│ ├── lib/
│ │ ├── Base64Utils.js
│ │ ├── Box.js
│ │ ├── Credentials.js
│ │ ├── CryptoUtils.js
│ │ ├── StorageService.js
│ │ └── Utils.js
│ ├── main.css
│ ├── main.html
│ ├── main.js
│ ├── postcss.config.js
│ ├── robots.txt
│ ├── routes.js
│ ├── stores.js
│ ├── tailwind.config.js
│ ├── views/
│ │ ├── ContentView.svelte
│ │ ├── ListView.svelte
│ │ └── UnlockView.svelte
│ └── webpack.config.js
├── auth0/
│ ├── 01-whitelist.js
│ ├── 02-notify.js
│ └── 03-wait-logic.js
├── bin/
│ ├── run
│ └── run.cmd
├── cli/
│ ├── commands/
│ │ ├── auth0/
│ │ │ └── sync.js
│ │ ├── build.js
│ │ ├── init.js
│ │ ├── pack.js
│ │ ├── regenerate-token.js
│ │ ├── url/
│ │ │ ├── add.js
│ │ │ ├── list.js
│ │ │ └── rm.js
│ │ ├── user/
│ │ │ ├── add.js
│ │ │ ├── list.js
│ │ │ └── rm.js
│ │ ├── wait-time/
│ │ │ ├── get.js
│ │ │ └── set.js
│ │ └── webhook/
│ │ ├── get.js
│ │ └── set.js
│ ├── index.js
│ └── lib/
│ ├── Auth0Management.js
│ ├── Builder.js
│ ├── Config.js
│ ├── Content.js
│ ├── Crypto.js
│ ├── Utils.js
│ └── aes-kw.js
├── docs-source/
│ ├── .gitignore
│ ├── config.yaml
│ ├── content/
│ │ ├── _index.md
│ │ ├── advanced/
│ │ │ ├── auth0-manual-configuration.md
│ │ │ ├── building-self-contained-binaries.md
│ │ │ ├── configuration-file.md
│ │ │ └── index-file.md
│ │ ├── cli/
│ │ │ └── __template.md
│ │ ├── guides/
│ │ │ ├── auth0-setup.md
│ │ │ ├── build-static-web-app.md
│ │ │ ├── create-box.md
│ │ │ ├── deploy-box.md
│ │ │ ├── get-started.md
│ │ │ ├── login-notifications.md
│ │ │ └── managing-users.md
│ │ ├── introduction/
│ │ │ ├── quickstart-video.md
│ │ │ └── security-model.md
│ │ └── menu/
│ │ └── __template.md
│ ├── generate-cli-docs.js
│ ├── sync-assets.sh
│ ├── themes/
│ │ └── book/
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── archetypes/
│ │ │ └── docs.md
│ │ ├── assets/
│ │ │ ├── _markdown.scss
│ │ │ ├── _utils.scss
│ │ │ ├── _variables.scss
│ │ │ └── book.scss
│ │ ├── layouts/
│ │ │ ├── 404.html
│ │ │ ├── docs/
│ │ │ │ ├── baseof.html
│ │ │ │ ├── list.html
│ │ │ │ └── single.html
│ │ │ ├── partials/
│ │ │ │ └── docs/
│ │ │ │ ├── brand.html
│ │ │ │ ├── git-footer.html
│ │ │ │ ├── html-head.html
│ │ │ │ ├── inject/
│ │ │ │ │ ├── body.html
│ │ │ │ │ ├── head.html
│ │ │ │ │ ├── menu-after.html
│ │ │ │ │ └── menu-before.html
│ │ │ │ ├── menu-bundle.html
│ │ │ │ ├── menu-filetree.html
│ │ │ │ ├── menu.html
│ │ │ │ ├── mobile-header.html
│ │ │ │ ├── shared.html
│ │ │ │ └── toc.html
│ │ │ └── posts/
│ │ │ ├── baseof.html
│ │ │ ├── list.html
│ │ │ └── single.html
│ │ ├── source
│ │ └── theme.toml
│ ├── workers-site/
│ │ ├── .cargo-ok
│ │ ├── .gitignore
│ │ ├── assets.js
│ │ ├── cache-config.js
│ │ ├── index.js
│ │ └── package.json
│ └── wrangler.toml
└── package.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintignore
================================================
# Docs
docs-source
# Auth0 rules (they follow a different style)
auth0
# Test data
testfolder
================================================
FILE: .eslintrc.js
================================================
module.exports = {
env: {
es6: true,
node: true,
browser: true
},
extends: 'eslint:recommended',
parserOptions: {
ecmaVersion: 2019,
sourceType: 'module'
},
plugins: [
'html',
'svelte3'
],
overrides: [
{
files: '**/*.svelte',
processor: 'svelte3/svelte3'
}
],
globals: {
// See https://github.com/eslint/eslint/issues/11524
BigInt: true
},
settings: {
'svelte3/ignore-styles': () => true,
'html': {
'indent': 0,
'report-bad-indent': 'warn',
'html-extensions': [
'.html'
]
}
},
rules: {
'indent': [
'error',
4,
{
SwitchCase: 1,
MemberExpression: 1,
ArrayExpression: 1,
ObjectExpression: 1
}
],
'linebreak-style': [
'error',
'unix'
],
'quotes': [
'error',
'single'
],
'semi': [
'error',
'never'
],
'quote-props': [
'warn',
'as-needed'
],
'no-var': [
'error'
],
'prefer-const': [
'warn'
],
'no-unused-vars': [
'error',
{
args: 'none'
}
],
'brace-style': [
'error',
'stroustrup',
{
allowSingleLine: false
}
],
'eol-last': [
'error',
'always'
],
'space-before-function-paren': [
'error',
{
anonymous: 'never',
named: 'never',
asyncArrow: 'always'
}
],
'keyword-spacing': [
'error',
{
before: true,
after: true
}
],
'key-spacing': [
'error',
{
beforeColon: false,
afterColon: true,
mode: 'strict'
}
],
'comma-spacing': [
'error'
],
'arrow-spacing': [
'error'
],
'array-bracket-spacing': [
'error',
'never',
{
singleValue: false,
objectsInArrays: true,
arraysInArrays: true
}
],
'curly': [
'error'
],
'space-infix-ops': [
'error',
{
int32Hint: false
}
],
'space-unary-ops': [
'error',
{
words: true,
nonwords: false
}
],
'space-before-blocks': [
'error'
],
'object-curly-spacing': [
'error',
'never'
],
'space-in-parens': [
'error',
'never'
],
'prefer-arrow-callback': [
'warn'
],
'no-return-await': [
'error'
],
'no-console': [
'warn'
],
'no-nested-ternary': [
'error'
],
'no-unneeded-ternary': [
'warn'
],
'no-unexpected-multiline': [
'error'
],
'lines-around-directive': [
'error',
'always'
],
// Need to disable this because it causes issues with Svelte
'no-multiple-empty-lines': 'off',
'operator-linebreak': [
'error',
'after'
]
}
}
================================================
FILE: .github/FUNDING.yml
================================================
github: ItalyPaleAle
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: italypaleale
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: ['https://keybase.io/italypaleale']
================================================
FILE: .github/workflows/docs-ci.yaml
================================================
# Docs: CI: deploys to dev
# Required secrets:
# CF_ACCOUNT_ID: Account ID for Cloudflare Workers
# CF_API_TOKEN: API token for Cloudflare (for the Workers CLI)
# AZURE_STORAGE_ACCOUNT: Name of the Azure Storage Account
# AZCOPY_SPA_APPLICATION_ID: Application ID (Client ID) for the Service Principal with access to the Azure Storage account
# AZCOPY_SPA_CLIENT_SECRET: Client Secret for the Service Principal
# AZCOPY_SPA_TENANT_ID: Tenant ID of the application (Service Principal)
name: 'Docs: CI'
on:
push:
branches:
- master
jobs:
build-and-deploy:
runs-on: 'ubuntu-20.04'
env:
# Version of Hugo to use
HUGO_VERSION: '0.68.1'
steps:
- name: 'Check out code'
uses: 'actions/checkout@v2'
- name: 'Install Node.js'
uses: 'actions/setup-node@v1'
with:
node-version: '14.x'
- name: 'Install npm deps'
run: |
npm ci
- name: 'Install Hugo'
run: |
cd docs-source
mkdir -p .bin
cd .bin
echo "Using Hugo ${HUGO_VERSION}"
curl -fsSL "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_Linux-64bit.tar.gz" -o hugo.tar.gz
tar -zxf hugo.tar.gz
- name: 'Build site'
run: |
# Build the site
cd docs-source
node generate-cli-docs.js
.bin/hugo
- name: 'Install azcopy and authenticate'
run: |
cd docs-source
mkdir -p .bin
curl -Ls "https://aka.ms/downloadazcopy-v10-linux" -o ".bin/azcopy.tar.gz"
(cd .bin && tar -xvzf azcopy.tar.gz --strip 1)
.bin/azcopy --version
.bin/azcopy login --service-principal --application-id $AZCOPY_SPA_APPLICATION_ID --tenant-id $AZCOPY_SPA_TENANT_ID
env:
# Service Principal credentials
AZCOPY_SPA_APPLICATION_ID: ${{ secrets.AZCOPY_SPA_APPLICATION_ID }}
AZCOPY_SPA_CLIENT_SECRET: ${{ secrets.AZCOPY_SPA_CLIENT_SECRET }}
AZCOPY_SPA_TENANT_ID: ${{ secrets.AZCOPY_SPA_TENANT_ID }}
- name: 'Upload static assets to Azure Storage'
run: |
cd docs-source
# Upload assets to Azure Storage
./sync-assets.sh
# Delete the assets from disk so they're not uploaded to Cloudflare or published as artifact
for asset in $ASSETS; do rm -rvf "$asset"; done
env:
# List of assets to upload
ASSETS: 'public/images public/svg'
# Container in Azure Storage
CONTAINER: 'hereditas-dev'
# Use azcopy downloaded above
AZCOPYCMD: '.bin/azcopy'
# Storage Account name
AZURE_STORAGE_ACCOUNT: ${{ secrets.AZURE_STORAGE_ACCOUNT }}
- name: 'Publish docs as artifact'
uses: 'actions/upload-artifact@v2'
with:
name: 'docs-dev'
path: 'docs-source/public'
- name: 'Deploy to dev environment'
uses: cloudflare/wrangler-action@1.3.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
workingDirectory: 'docs-source'
env:
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
================================================
FILE: .github/workflows/docs-production.yaml
================================================
# Docs: Deploys to production, triggered manually
# Required secrets:
# CF_ACCOUNT_ID: Account ID for Cloudflare Workers
# CF_API_TOKEN: API token for Cloudflare (for the Workers CLI)
# CF_ZONE_ID: Zone ID for the Cloudflare domain
# AZURE_STORAGE_ACCOUNT: Name of the Azure Storage Account
# AZCOPY_SPA_APPLICATION_ID: Application ID (Client ID) for the Service Principal with access to the Azure Storage account
# AZCOPY_SPA_CLIENT_SECRET: Client Secret for the Service Principal
# AZCOPY_SPA_TENANT_ID: Tenant ID of the application (Service Principal)
name: 'Docs: Production'
on:
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: 'ubuntu-20.04'
env:
# Version of Hugo to use
HUGO_VERSION: '0.68.1'
steps:
- name: 'Check out code'
uses: 'actions/checkout@v2'
- name: 'Install Node.js'
uses: 'actions/setup-node@v1'
with:
node-version: '14.x'
- name: 'Install npm deps'
run: |
npm ci
- name: 'Install Hugo'
run: |
cd docs-source
mkdir -p .bin
cd .bin
echo "Using Hugo ${HUGO_VERSION}"
curl -fsSL "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_Linux-64bit.tar.gz" -o hugo.tar.gz
tar -zxf hugo.tar.gz
- name: 'Build site'
run: |
# Build the site
cd docs-source
node generate-cli-docs.js
.bin/hugo
- name: 'Install azcopy and authenticate'
run: |
cd docs-source
mkdir -p .bin
curl -Ls "https://aka.ms/downloadazcopy-v10-linux" -o ".bin/azcopy.tar.gz"
(cd .bin && tar -xvzf azcopy.tar.gz --strip 1)
.bin/azcopy --version
.bin/azcopy login --service-principal --application-id $AZCOPY_SPA_APPLICATION_ID --tenant-id $AZCOPY_SPA_TENANT_ID
env:
# Service Principal credentials
AZCOPY_SPA_APPLICATION_ID: ${{ secrets.AZCOPY_SPA_APPLICATION_ID }}
AZCOPY_SPA_CLIENT_SECRET: ${{ secrets.AZCOPY_SPA_CLIENT_SECRET }}
AZCOPY_SPA_TENANT_ID: ${{ secrets.AZCOPY_SPA_TENANT_ID }}
- name: 'Upload static assets to Azure Storage'
run: |
cd docs-source
# Upload assets to Azure Storage
./sync-assets.sh
# Delete the assets from disk so they're not uploaded to Cloudflare or published as artifact
for asset in $ASSETS; do rm -rvf "$asset"; done
env:
# List of assets to upload
ASSETS: 'public/images public/svg'
# Container in Azure Storage
CONTAINER: 'hereditas-prod'
# Use azcopy downloaded above
AZCOPYCMD: '.bin/azcopy'
# Storage Account name
AZURE_STORAGE_ACCOUNT: ${{ secrets.AZURE_STORAGE_ACCOUNT }}
- name: 'Publish docs as artifact'
uses: 'actions/upload-artifact@v2'
with:
name: 'docs-prod'
path: 'docs-source/public'
- name: 'Deploy to production environment'
uses: cloudflare/wrangler-action@1.3.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
environment: 'production'
workingDirectory: 'docs-source'
env:
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
================================================
FILE: .github/workflows/docs-staging.yaml
================================================
# Docs: Deploys to staging, triggered manually
# Required secrets:
# CF_ACCOUNT_ID: Account ID for Cloudflare Workers
# CF_API_TOKEN: API token for Cloudflare (for the Workers CLI)
# CF_ZONE_ID: Zone ID for the Cloudflare domain
# AZURE_STORAGE_ACCOUNT: Name of the Azure Storage Account
# AZCOPY_SPA_APPLICATION_ID: Application ID (Client ID) for the Service Principal with access to the Azure Storage account
# AZCOPY_SPA_CLIENT_SECRET: Client Secret for the Service Principal
# AZCOPY_SPA_TENANT_ID: Tenant ID of the application (Service Principal)
name: 'Docs: Staging'
on:
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: 'ubuntu-20.04'
env:
# Version of Hugo to use
HUGO_VERSION: '0.68.1'
steps:
- name: 'Check out code'
uses: 'actions/checkout@v2'
- name: 'Install Node.js'
uses: 'actions/setup-node@v1'
with:
node-version: '14.x'
- name: 'Install npm deps'
run: |
npm ci
- name: 'Install Hugo'
run: |
cd docs-source
mkdir -p .bin
cd .bin
echo "Using Hugo ${HUGO_VERSION}"
curl -fsSL "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_Linux-64bit.tar.gz" -o hugo.tar.gz
tar -zxf hugo.tar.gz
- name: 'Build site'
run: |
# Build the site
cd docs-source
node generate-cli-docs.js
.bin/hugo
- name: 'Install azcopy and authenticate'
run: |
cd docs-source
mkdir -p .bin
curl -Ls "https://aka.ms/downloadazcopy-v10-linux" -o ".bin/azcopy.tar.gz"
(cd .bin && tar -xvzf azcopy.tar.gz --strip 1)
.bin/azcopy --version
.bin/azcopy login --service-principal --application-id $AZCOPY_SPA_APPLICATION_ID --tenant-id $AZCOPY_SPA_TENANT_ID
env:
# Service Principal credentials
AZCOPY_SPA_APPLICATION_ID: ${{ secrets.AZCOPY_SPA_APPLICATION_ID }}
AZCOPY_SPA_CLIENT_SECRET: ${{ secrets.AZCOPY_SPA_CLIENT_SECRET }}
AZCOPY_SPA_TENANT_ID: ${{ secrets.AZCOPY_SPA_TENANT_ID }}
- name: 'Upload static assets to Azure Storage'
run: |
cd docs-source
# Upload assets to Azure Storage
./sync-assets.sh
# Delete the assets from disk so they're not uploaded to Cloudflare or published as artifact
for asset in $ASSETS; do rm -rvf "$asset"; done
env:
# List of assets to upload
ASSETS: 'public/images public/svg'
# Container in Azure Storage
CONTAINER: 'hereditas-staging'
# Use azcopy downloaded above
AZCOPYCMD: '.bin/azcopy'
# Storage Account name
AZURE_STORAGE_ACCOUNT: ${{ secrets.AZURE_STORAGE_ACCOUNT }}
- name: 'Publish docs as artifact'
uses: 'actions/upload-artifact@v2'
with:
name: 'docs-staging'
path: 'docs-source/public'
- name: 'Deploy to staging environment'
uses: cloudflare/wrangler-action@1.3.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
environment: 'staging'
workingDirectory: 'docs-source'
env:
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
================================================
FILE: .gitignore
================================================
# Test data
testfolder
# OClif manifest
oclif.manifest.json
# Created by https://www.gitignore.io/api/node,macos,linux,windows,visualstudiocode
# Edit at https://www.gitignore.io/?templates=node,macos,linux,windows,visualstudiocode
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
### Windows ###
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.gitignore.io/api/node,macos,linux,windows,visualstudiocode
================================================
FILE: .npmignore
================================================
.DS_Store
assets/
.vscode/
docs-source/
testfolder/
azure-pipelines.yaml
================================================
FILE: .vscode/settings.json
================================================
{
"files.exclude": {
"**/node_modules": true,
"coverage": true,
"coverage.lcov": true
},
"files.trimTrailingWhitespace": true,
"editor.tabSize": 4,
"files.insertFinalNewline": true,
"eslint.options": {
"configFile": ".eslintrc.yml"
},
"eslint.validate": [ "javascript", "html", "svelte" ]
}
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team. All complaints will be reviewed and
investigated and will result in a response that is deemed necessary and appropriate
to the circumstances. The project team is obligated to maintain confidentiality
with regard to the reporter of an incident. Further details of specific enforcement
policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
================================================
FILE: LICENSE.md
================================================
# License
Copyright © 2019-2020 Alessandro Segala @ItalyPaleAle.
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
````text
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
````
================================================
FILE: README.md
================================================
# Hereditas
[](https://open.vscode.dev/ItalyPaleAle/hereditas)
[](https://npmjs.org/package/hereditas)
[](https://npmjs.org/package/hereditas)
[](https://github.com/ItalyPaleAle/hereditas/blob/master/package.json)
## What happens to your digital life after you're gone?

Hereditas, which means *inheritance* in Latin, is a static website generator that builds **fully-trustless digital legacy boxes**, where you can store information for your relatives to access in case of your sudden death or disappearance.
For example, you could use this to pass information such as passwords, cryptographic keys, cryptocurrency wallets, sensitive documents, etc.
## Learn more
Read the [Hereditas announcement](https://withblue.ink/2019/03/18/what-happens-to-your-digital-life-after-youre-gone-introducing-hereditas.html?utm_source=web&utm_campaign=hereditas-github) to understand more on why we need Hereditas.
You can also watch this short [intro video](https://www.youtube.com/watch?v=lZEKgB5dzQ4).
## Get started and documentation
❓ [**What is Hereditas**](https://hereditas.app)
🚀 [**Get started guide**](https://hereditas.app/guides/get-started.html)
🔐 [**Security model**](https://hereditas.app/introduction/security-model.html)
📘 [**Documentation and CLI reference**](https://hereditas.app)
## Screenshot

## Warning: alpha quality software
**Hereditas is currently alpha quality software; use at your own risk.** While we've developed Hereditas with security always as the top priority, this software leverages a lot of cryptographic primitives under the hood. We won't release a stable (e.g. "1.0") version of Hereditas until we're confident that enough people and cryptography experts have audited and improved the code.
**Your help is highly appreciated.** If you are an expert on security or cryptography, please help us reviewing the code and let us know what you think - including if everything looks fine, or if you found a bug.
Responsible disclosure: if you believe you've found a security issue that could compromise current users of Hereditas, please [report it confidentially](https://www.npmjs.com/advisories/report?package=hereditas).
## License
Copyright © 2020, Alessandro Segala @ItalyPaleAle
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You can read the full text of the license in the [LICENSE.md](./LICENSE.md) file.
================================================
FILE: app/components/NavBar.svelte
================================================
<nav class="bg-white fixed w-full z-10 top-0 shadow">
<div class="w-full lg:w-3/5 container px-2 flex flex-wrap items-center justify-between my-4">
<div class="pl-4 md:pl-0">
<span class="flex items-center text-blue-600 text-base xl:text-xl no-underline hover:no-underline font-extrabold font-sans">{$pageTitle}</span>
</div>
</div>
</nav>
<script>
// Stores
import {pageTitle} from '../stores'
</script>
================================================
FILE: app/components/PassphraseBox.svelte
================================================
{#await $box.fetchIndex()}
<p>Fetching index, please wait…</p>
{:then response}
<form class="w-full max-w-md" on:submit|preventDefault={handleSubmit}>
<div class="md:flex md:items-center mb-6">
<div class="md:w-1/3">
<label class="block md:text-right mb-1 md:mb-0 pr-4" for="inputPassphrase">
Unlock passphrase:
</label>
</div>
<div class="md:w-2/3">
<input class="bg-white appearance-none border {unlockError ? 'border-red-500' : 'border-gray-200'} rounded w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-blue-500" id="inputPassphrase" bind:value={passphrase} type="password" placeholder="•••••••" />
{#if unlockError}
<p class="text-xs text-red-500">This passphrase isn't correct</p>
{/if}
</div>
</div>
<div class="md:flex md:items-center">
<div class="md:w-1/3"></div>
<div class="md:w-2/3">
<button class="bg-blue-500 hover:bg-blue-700 text-white no-underline font-bold py-2 px-4 rounded" type="submit">
Browse this Hereditas
</button>
</div>
</div>
</form>
{:catch error}
<p>Error while fetching the index: {error}</p>
{/await}
<script>
// Libs
import {replace} from 'svelte-spa-router'
// Stores
import {box, hereditasProfile} from '../stores'
// Props
let passphrase = ''
let unlockError = false
// Form submit handler
function handleSubmit() {
unlockError = false
$box.unlock(passphrase, $hereditasProfile.token)
.then((_) => {
unlockError = false
// Redirect to the list
replace('/list')
})
.catch((err) => {
// eslint-disable-next-line no-console
console.error('err caught', err)
unlockError = true
})
}
</script>
================================================
FILE: app/components/RequestAuthentication.svelte
================================================
{#if $authError}
<h1>Authentication error</h1>
<p class="mb-2"><b>Error description:</b> {$authError}</p>
<a href={authUrl} class="bg-blue-500 hover:bg-blue-700 text-white no-underline font-bold py-2 px-4 rounded">Try authenticating again</a>
{:else}
<h1>Authenticate with this Hereditas box</h1>
<a href={authUrl} class="bg-blue-500 hover:bg-blue-700 text-white no-underline font-bold py-2 px-4 rounded">Authenticate</a>
{/if}
<script>
// Libs
import credentials from '../lib/Credentials'
// Stores
import {profile, authError} from '../stores'
// Generate authentication url
let authUrl = undefined
$: {
// If we're not authenticated, request a URL
authUrl = $profile ? undefined : credentials.authorizationUrl()
}
</script>
================================================
FILE: app/components/UserProfile.svelte
================================================
<h1>Hello, {$profile.name}!</h1>
{#if $hereditasProfile.role == 'owner'}
<p class="mb-2">You're the owner of this Hereditas box, so you can unlock it at any time.</p>
<p class="mx-2 my-4 p-2 border border-blue-600 bg-blue-400 text-white shadow" role="alert">By logging in, you have stopped the timer for the waiting period before other users can unlock your box.</p>
<PassphraseBox />
{:else}
{#if $hereditasProfile.token}
<p class="mb-2">You can now access to the content of this Hereditas.</p>
<PassphraseBox />
{:else}
<p class="m-2 p-2 border border-blue-600 bg-blue-400 text-white shadow" role="alert">Thanks for requesting access. This Hereditas box will be unlocked on <b>{unlockedDate.toLocaleString().replace(/ /g, '\xa0')}</b>. Please check later.<br/>
Important: if an owner signs in with their account, this Hereditas will be locked again.</p>
{/if}
{/if}
<script>
// Components
import PassphraseBox from './PassphraseBox.svelte'
// Stores
import {profile, hereditasProfile} from '../stores'
// Unlocked date
let unlockedDate = new Date()
$: {
// Get the date at which this Hereditas instance is unlocked
unlockedDate = new Date(($hereditasProfile.requestTime + $hereditasProfile.waitTime) * 1000)
}
</script>
================================================
FILE: app/layout/App.svelte
================================================
<Navbar />
<div class="container w-full lg:w-3/5 px-2 pt-10 lg:pt-10 mt-10">
<Router {routes}/>
<footer class="text-xs text-gray-600 text-center mt-8 mb-2">Built with <a href="https://hereditas.app">Hereditas</a></footer>
</div>
<script>
// Components
import Navbar from '../components/Navbar.svelte'
// Router and routes
import Router from 'svelte-spa-router'
import routes from '../routes'
</script>
================================================
FILE: app/lib/Base64Utils.js
================================================
// Based on: https://github.com/danguer/blog-examples/blob/master/js/base64-binary.js
/*
Copyright (c) 2011, Daniel Guerrero
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL DANIEL GUERRERO BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/*
* Uses the new array typed in javascript to binary base64 encode/decode
* at the moment just decodes a binary base64 encoded
* into either an ArrayBuffer (decodeArrayBuffer)
* or into an Uint8Array (decode)
*
* References:
* https://developer.mozilla.org/en/JavaScript_typed_arrays/ArrayBuffer
* https://developer.mozilla.org/en/JavaScript_typed_arrays/Uint8Array
*/
const keyStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
/* will return a Uint8Array type */
export function DecodeArrayBuffer(input) {
input = RemovePaddingChars(input)
const bytes = (input.length / 4) * 3
const ab = new ArrayBuffer(bytes)
Decode(input, ab)
return ab
}
function RemovePaddingChars(input) {
for (let i = 0; i < Math.min(2, input.length); i++) {
if (input.charAt(input.length - 1) == keyStr[64]) {
input = input.substring(0, input.length - 1)
}
}
return input
}
export function Decode(input, arrayBuffer) {
input = RemovePaddingChars(input)
const bytes = parseInt((input.length / 4) * 3, 10)
let uarray
let chr1, chr2, chr3
let enc1, enc2, enc3, enc4
let i = 0
let j = 0
if (arrayBuffer) {
uarray = new Uint8Array(arrayBuffer)
}
else {
uarray = new Uint8Array(bytes)
}
input = input.replace(/[^A-Za-z0-9+/=]/g, '')
for (i = 0; i < bytes; i += 3) {
//get the 3 octects in 4 ascii chars
enc1 = keyStr.indexOf(input.charAt(j++))
enc2 = keyStr.indexOf(input.charAt(j++))
enc3 = keyStr.indexOf(input.charAt(j++))
enc4 = keyStr.indexOf(input.charAt(j++))
chr1 = (enc1 << 2) | (enc2 >> 4)
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2)
chr3 = ((enc3 & 3) << 6) | enc4
uarray[i] = chr1
if (enc3 != 64) {
uarray[i + 1] = chr2
}
if (enc4 != 64) {
uarray[i + 2] = chr3
}
}
return uarray
}
================================================
FILE: app/lib/Box.js
================================================
import {Decrypt, buf2str, UnwrapKey, DeriveKeyArgon2, DeriveKeyPBKDF2} from './CryptoUtils'
import {DecodeArrayBuffer} from './Base64Utils'
/**
* Manages the Hereditas box
*/
export class Box {
constructor() {
this._masterKey = null
this._contents = null
this._indexFetchingPromise = null
this._encryptedIndex = null
}
/**
* Returns true if the box is unlocked
*
* @returns {boolean}
*/
isUnlocked() {
return this._masterKey && this._contents
}
/**
* Lock the box again, removing the key and the decrypted index from memory
*/
lock() {
this._masterKey = null
this._contents = null
}
/**
* Returns decrypted index
* @returns {Array}
*/
getContents() {
return this.isUnlocked() ?
this._contents :
[]
}
/**
* Fetches a content from the box, then decrypts it before returning.
*
* @param {Object} info - Info for the content to retrieve. Must contain the `dist` and `tag` properties.
* @returns {Promise<Object>} Promise that resolves with info object containing the decrypted content, as a binary ArrayBuffer in the `info.data` property, or an utf-8 encoded string in the `info.text` property (if `info.display` is "text" or "html").
* @async
*/
fetchContent(info) {
// If the box is locked, return
if (!this.isUnlocked()) {
return Promise.reject('Box is locked')
}
// Ensure we have what we need
if (!info || !info.dist || !info.tag) {
return Promise.reject('Content not found')
}
// Return the promise
let iv = null
let data = null
return fetch(info.dist)
// Grab the encrypted contents as ArrayBuffer
.then((response) => response.arrayBuffer())
// Decrypt the data
.then((buffer) => {
// The first 40 bytes are the wrapped key, and the next 12 bytes are the IV
const wrappedKey = buffer.slice(0, 40)
iv = buffer.slice(40, 52)
data = buffer.slice(52)
// Un-wrap the key
return UnwrapKey(this._masterKey, wrappedKey)
})
.then((key) => {
// Get the tag
const tag = DecodeArrayBuffer(info.tag)
return Decrypt(key, iv, data, tag)
.then((data) => {
// Clone the info object
info = JSON.parse(JSON.stringify(info))
// If it's text, decode it
if (info.display == 'text' || info.display == 'html') {
info.text = buf2str(new Uint8Array(data))
}
else {
info.data = data
}
return info
})
})
}
/**
* Fetches the index from the box.
*
* @returns {Promise<void>} Promise that resolves (with no value) when the index has been fetched
* @async
*/
fetchIndex() {
// If we have the index already, do nothing
if (this._encryptedIndex) {
return Promise.resolve()
}
// If we're already fetching the index, return the promise
if (this._indexFetchingPromise) {
return this._indexFetchingPromise
}
// Fetch the index
this._indexFetchingPromise = fetch('_index')
// Grab the contents as ArrayBuffer
.then((response) => response.arrayBuffer())
// Store the results in the object
.then((buffer) => {
// Read the data from the response
this._encryptedIndex = {
// The first 40 bytes are the wrapped key, and the next 12 bytes are the IV
wrappedKey: buffer.slice(0, 40),
iv: buffer.slice(40, 52),
data: buffer.slice(52)
}
// Request is done
this._indexFetchingPromise = null
})
// Return the promise
return this._indexFetchingPromise
}
/**
* Attempts to decrypt the data using the passphrase and the app token
*
* @param {string} passphrase - Passphrase typed by the user
* @param {string} appToken - Encryption token for the app
* @async
* @throws Throws an exception if the decryption fails, which usually means that the key/passphrase is wrong
*/
unlock(passphrase, appToken) {
if (!passphrase || !appToken) {
return Promise.reject('Empty passphrase or app token')
}
// If we haven't fetched the index yet, return
if (!this._encryptedIndex) {
return Promise.resolve(false)
}
// Convert from Base64 to ArrayBuffer
const keySalt = DecodeArrayBuffer(process.env.KEY_SALT)
const indexTag = DecodeArrayBuffer(process.env.INDEX_TAG)
// Key derivation function: PBKDF2 or Argon2
let kdf
if (process.env.KEY_DERIVATION_FUNCTION == 'pbkdf2') {
kdf = DeriveKeyPBKDF2
}
else if (process.env.KEY_DERIVATION_FUNCTION == 'argon2') {
kdf = DeriveKeyArgon2
}
else {
throw Error('Invalid key derivation function requested')
}
// Try decrypting the index: this will verify the passphrase too
return Promise.resolve()
// First: derive the encryption key
.then(() => kdf(passphrase + appToken, keySalt))
.then((masterKey) => {
this._masterKey = masterKey
})
// Un-wrap the key
.then(() => UnwrapKey(this._masterKey, this._encryptedIndex.wrappedKey))
// Decrypt the index
.then((key) => Decrypt(key, this._encryptedIndex.iv, this._encryptedIndex.data, indexTag))
.then((data) => {
// Convert the buffer to string
const str = buf2str(new Uint8Array(data))
// Store the contents
this._contents = JSON.parse(str)
})
// Exceptions likely mean that the key/passphrase are wrong
.catch((err) => {
// eslint-disable-next-line no-console
console.error('Error while unlocking the box:', err)
// Ensure the box remains locked
this.lock()
// Bubble up
throw Error('Failed to unlock to box')
})
}
}
================================================
FILE: app/lib/Credentials.js
================================================
import {RandomString} from './Utils'
import storage from './StorageService'
import IdTokenVerifier from 'idtoken-verifier'
/**
* During the authentication process we need to use nonce's to protect against certain kinds of attacks.
*/
class Nonce {
constructor() {
this._nonceKeyName = 'hereditas-nonce'
this._nonceLength = 7
}
/**
* Generates a new nonce and stores it in the session storage
*
* @returns {string} A nonce
*/
generate() {
// Generate a nonce
const nonce = RandomString(this._nonceLength)
// Store the nonce in the session
storage.sessionStorage.setItem(this._nonceKeyName, nonce)
return nonce
}
/**
* Retrieves the last nonce from session storage
*
* @returns {string} A nonce
*/
retrieve() {
const read = storage.sessionStorage.getItem(this._nonceKeyName)
const regExp = new RegExp('^[A-Za-z0-9_\\-]{' + this._nonceLength + '}$')
if (!read || !read.match(regExp)) {
return null
}
return read
}
}
/**
* Managed the authentication flow, and validates the JWT token.
*/
export class Credentials {
constructor() {
this._sessionKeyName = 'hereditas-jwt'
this._tokenValidated = false
this._nonce = new Nonce()
this._profile = null
}
/**
* Returns the authorization URL to point users to, storing the nonce
*
* @returns {string} Authorization URL
*/
authorizationUrl() {
// Generate a nonce
const nonce = this._nonce.generate()
// URL-encode the return URL
const appUrl = encodeURIComponent(window.location.href)
// Generate the URL
const authIssuer = process.env.AUTH_ISSUER
const authClientId = process.env.AUTH_CLIENT_ID
return `${authIssuer}/authorize?client_id=${authClientId}&response_type=id_token&redirect_uri=${appUrl}&scope=openid%20profile&nonce=${nonce}&response_mode=fragment`
}
/**
* Returns the profile object from the JWT token
*
* @returns {Object} Profile for the authenticated user
* @async
*/
async getProfile() {
// If we have a pre-parsed and pre-validated profile in memory, return that
if (this._profile) {
return this._profile
}
// Get the token
const jwt = this.getToken()
if (!jwt) {
return {}
}
// Get the profile out of the token
let profile
try {
profile = await this._validateToken(jwt)
if (!profile) {
profile = {}
}
this._profile = profile
return profile
}
catch (e) {
this._profile = {}
throw e
}
}
/**
* Returns the JWT token for the session
*
* @returns {string|null} JWT Token, or null if no token
*/
getToken() {
const read = storage.sessionStorage.getItem(this._sessionKeyName)
if (!read || !read.length) {
return null
}
let data
try {
data = JSON.parse(read)
}
catch (error) {
// eslint-disable-next-line no-console
console.error('Error while parsing JSON from sessionStorage', error)
throw Error('Could not get the token from session storage')
}
if (!data || !data.jwt) {
return null
}
return data.jwt
}
/**
* Stores the JWT token for the session
*
* @param {string} jwt - JWT Token
* @async
*/
async setToken(jwt) {
// Delete the profile in memory
this._profile = null
// First, validate the token
const profile = await this._validateToken(jwt)
if (!profile) {
throw Error('Token validation failed')
}
// Store the token
storage.sessionStorage.setItem(this._sessionKeyName, JSON.stringify({jwt}))
// Set the profile in memory
this._profile = profile
}
/**
* Validates a token
*
* @param {string} jwt - JWT token to validate
* @returns {Promise<Object>} Extracted payload
* @private
*/
async _validateToken(jwt) {
// Ensure issuer ends with /
const issuer = process.env.AUTH_ISSUER + (process.env.AUTH_ISSUER.charAt(process.env.AUTH_ISSUER.length - 1) != '/' ? '/' : '')
// Validate the token
const verifier = new IdTokenVerifier({
issuer,
audience: process.env.AUTH_CLIENT_ID
})
const payload = await new Promise((resolve, reject) => {
verifier.verify(jwt, this._nonce.retrieve(), (error, payload) => {
if (error) {
// eslint-disable-next-line no-console
console.error('Validation error', error)
return reject('Invalid token')
}
// Check if the payload contains the Hereditas namespace
if (!payload[process.env.ID_TOKEN_NAMESPACE]) {
// eslint-disable-next-line no-console
console.error('Token doesn\'t contain the Hereditas namespace')
return reject('Token doesn\'t contain the Hereditas namespace')
}
resolve(payload)
})
})
return payload
}
}
// The default export is an instance (singleton) of Credentials
const credentials = new Credentials()
export default credentials
================================================
FILE: app/lib/CryptoUtils.js
================================================
// Inspired by https://gist.github.com/tscholl2/dc7dc15dc132ea70a98e8542fefffa28#file-aes-js
/**
* Encodes a utf8 string as a byte array.
* @param {String} str
* @returns {Uint8Array}
*/
export function str2buf(str) {
return new TextEncoder('utf-8').encode(str)
}
/**
* Decodes a byte array as a utf8 string.
* @param {Uint8Array} buffer
* @returns {String}
*/
export function buf2str(buffer) {
return new TextDecoder('utf-8').decode(buffer)
}
/**
* Conctatenates two ArrayBuffer's
*
* @param {ArrayBuffer} buffer1 - First buffer
* @param {ArrayBuffer} buffer2 - Second buffer
* @returns {ArrayBuffer} The buffer with the data concatenated
*/
function concatBuffers(buffer1, buffer2) {
const result = new Uint8Array(buffer1.byteLength + buffer2.byteLength)
result.set(new Uint8Array(buffer1), 0)
result.set(new Uint8Array(buffer2), buffer1.byteLength)
return result.buffer
}
/**
* Given a passphrase and a salt, this generates a crypto key
* using Argon2.
* @param {string} passphrase
* @param {ArrayBuffer} salt
* @returns {Promise<CryptoKey>}
* @async
*/
export function DeriveKeyArgon2(passphrase, salt) {
// Import argon2 dynamically to reduce bundle size, if it's not necessary
const saltArr = new Uint8Array(salt)
return import('argon2-browser')
.then((argon2) => argon2.hash({
pass: passphrase,
salt: saltArr,
type: argon2.ArgonType.Argon2id,
time: process.env.ARGON2_ITERATIONS,
mem: process.env.ARGON2_MEMORY,
hashLen: 32,
parallelism: 1
}))
.then((res) =>
window.crypto.subtle.importKey(
'raw',
res.hash,
{name: 'AES-KW', length: 256},
false,
['unwrapKey']
)
)
}
/**
* Given a passphrase and a salt, this generates a crypto key
* using `PBKDF2` with SHA-512 and N iterations.
* @param {string} passphrase
* @param {ArrayBuffer} salt
* @returns {Promise<CryptoKey>}
* @async
*/
export function DeriveKeyPBKDF2(passphrase, salt) {
return Promise.resolve()
.then(() =>
window.crypto.subtle.importKey(
'raw',
str2buf(passphrase),
'PBKDF2',
false,
['deriveKey']
)
)
.then((k) =>
window.crypto.subtle.deriveKey(
{name: 'PBKDF2', salt, iterations: process.env.PBKDF2_ITERATIONS, hash: 'SHA-512'},
k,
{name: 'AES-KW', length: 256},
false,
['unwrapKey']
)
)
}
/**
* Given a key and ciphertext (in the form of a string) as given by `encrypt`,
* this decrypts the ciphertext and returns the original plaintext
* @param {CryptoKey} key - Encryption key
* @param {ArrayBuffer} iv - IV
* @param {ArrayBuffer} data - Data to decrypt
* @param {ArrayBuffer} tag - AES-GCM tag
* @returns {Promise<string>} Decrypted text as string
* @async
* @throws Throws an error if the decryption fails, likely meaning that the key was wrong.
*/
export function Decrypt(key, iv, data, tag) {
return window.crypto.subtle.decrypt(
{name: 'AES-GCM', iv},
key,
concatBuffers(data, tag)
)
}
/**
* Unwraps a key wrapped with AES-KW (per RFC 3349)
*
* @param {CryptoKey} wrappingKey - Key used to wrap/unwrap the key
* @param {ArrayBuffer} ciphertext - Wrapped key
* @returns {Promise<CryptoKey>} Unwrapped key
* @async
* @throws Throws an error if the decryption fails, likely meaning that the key was wrong.
*/
export function UnwrapKey(wrappingKey, ciphertext) {
return window.crypto.subtle.unwrapKey(
'raw',
ciphertext,
wrappingKey,
{name: 'AES-KW'},
{name: 'AES-GCM'},
false,
['decrypt']
)
.then((key) => {
return key
})
}
================================================
FILE: app/lib/StorageService.js
================================================
// This module is based on https://github.com/Acanguven/StorageService/blob/master/storage.js
// License: MIT https://github.com/Acanguven/StorageService/blob/master/LICENSE
/**
* This class allows access to localStorage and sessionStorage.
* If they are not supported in the current browser, automatically falls back to a cookie-based storage
*/
export class StorageService {
/**
* Initializes the object.
*/
constructor() {
this.localStorage = this._isSupported('localStorage') ?
window.localStorage :
new CookieStore()
this.sessionStorage = this._isSupported('sessionStorage') ?
window.sessionStorage :
new CookieStore(true)
}
/**
* Tests if the type of storage is supported in the current browser
*
* @param {"localStorage"|"sessionStorage"} type - Name of the storage to test
* @returns {boolean} True if the browser supports the kind of storage
* @private
*/
_isSupported(type) {
const testKey = '__isSupported'
const storage = window[type]
try {
storage.setItem(testKey, '1')
storage.removeItem(testKey)
return true
}
catch (error) {
return false
}
}
}
/**
* Interface that implements the protocol of localStorage/sessionStorage while keeping the data in memory.
*/
export class MemoryStore {
/**
* Initializes the object.
*/
constructor() {
this._store = {}
}
getItem(name) {
return this._store[name] || null
}
setItem(name, value) {
this._store[name] = value
}
removeItem(name) {
delete this._store[name]
}
}
/**
* Interface that implements the protocol of localStorage/sessionStorage while keeping the data in a cookie.
*/
export class CookieStore {
/**
* Initializes the object.
*
* @param {bool} isSessionStorage - True if this object is for session storage (controls cookies' expiry)
*/
constructor(isSessionStorage) {
this._objectStore = {}
this._expireDate = isSessionStorage ?
' path=/' :
' expires=Tue, 19 Jan 2038 03:14:07 GMT path=/'
this._updateObject()
}
getItem(name) {
return this._objectStore[name] || null
}
setItem(name, value) {
if (!name) {
return
}
document.cookie = escape(name) + '=' + escape(value) + this._expireDate
this._updateObject()
}
removeItem(name) {
if (!name) {
return
}
document.cookie = escape(name) + this._expireDate
delete this._objectStore[name]
}
_updateObject() {
const couples = document.cookie.split(/\s*\s*/)
for (let i = 0; i < couples.length; i++) {
const couple = couples[i].split(/\s*=\s*/)
if (couple.length > 1) {
const key = unescape(couple[0])
this._objectStore[key] = unescape(couple[1])
}
}
}
}
const storage = new StorageService()
export default storage
================================================
FILE: app/lib/Utils.js
================================================
/**
* Returns a random string, useful for example as nonce.
*
* @param {number} length - Length of the string
* @returns {string} Random string
*/
export function RandomString(length = 7) {
const bytes = new Uint8Array(length)
const random = window.crypto.getRandomValues(bytes)
const result = []
const charset = '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-_'
random.forEach((c) => {
result.push(charset[c % charset.length])
})
return result.join('')
}
/**
* Returns a Promise that resolves after a certain amount of time, in ms
*/
export function WaitPromise(time) {
return new Promise((resolve) => {
setTimeout(resolve, time || 0)
})
}
================================================
FILE: app/main.css
================================================
/* Tailwind */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Default styles */
h1 {
@apply text-2xl mb-2;
}
h2 {
@apply text-xl mb-2;
}
h3 {
@apply text-lg mb-2;
}
a {
@apply text-blue-600 underline;
}
/* Rendered content */
section.rendered {
@apply my-2 mx-4 px-3 py-1 bg-white border border-blue-500;
}
.rendered h1, .rendered h2, .rendered h3, .rendered h4, .rendered h5, .rendered h6 {
@apply mt-4 mb-2;
}
.rendered p {
@apply my-2;
}
.rendered pre {
@apply mx-2;
}
.rendered code {
@apply w-full whitespace-pre-wrap text-sm;
}
.rendered ul {
@apply list-disc list-inside;
}
.rendered ol {
@apply list-decimal list-inside;
}
.rendered ul li, .rendered ol li {
@apply pl-6;
}
.rendered blockquote {
@apply italic px-6 text-sm;
}
================================================
FILE: app/main.html
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<!-- Title -->
<title>Hereditas</title>
</head>
<body class="bg-gray-100 text-gray-900 tracking-wider leading-normal">
</body>
</html>
================================================
FILE: app/main.js
================================================
// Style
import './main.css'
// JavaScript modules
import App from './layout/App.svelte'
import credentials from './lib/Credentials'
import qs from 'qs'
import {Box} from './lib/Box'
// Import stores
import {profile, hereditasProfile, box, authError} from './stores'
function getHash() {
let hash = window.location.hash
if (hash && hash.length > 2) {
// Remove the leading # and / characters
if (hash.charAt(0) == '#') {
hash = hash.substr(1)
}
if (hash.charAt(0) == '/') {
hash = hash.substr(1)
}
const parsed = qs.parse(hash, {
depth: 1,
parameterLimit: 20,
ignoreQueryPrefix: true,
})
// Remove the information from the URL (for security, in case it contains an id_token)
history.replaceState(undefined, undefined, '#')
return parsed
}
else {
return null
}
}
function checkAuthError(hash) {
// Check if we have an error from the authentication server
if (hash && hash.error) {
// Check for the error type
if (hash.error == 'unauthorized') {
return hash.error_description || 'Unauthorized'
}
else {
return hash.error_description || hash.error
}
}
return null
}
async function handleSession(hash) {
// Check if we have an id_token
if (hash && hash.id_token) {
// Validate and store the JWT
// If there's an error, redirect to auth page
try {
await credentials.setToken(hash.id_token)
}
catch (error) {
// eslint-disable-next-line no-console
console.error('Token error', error)
throw Error('Token error')
}
}
// If we have credentials stored, redirect the user to the authentication page
if (!credentials.getToken()) {
return false
}
// Get the profile
// If there's no session or it has expired, redirect to auth page
let profileData
try {
profileData = await credentials.getProfile()
}
catch (error) {
// eslint-disable-next-line no-console
console.error('Token error', error)
throw Error('Token error')
}
return profileData
}
const app = (async function() {
let _profile
let _hereditasProfile = null
let _box = null
// Parse the hash if any
const hash = getHash()
// Check if we have an error from the authentication server
let unrecoverableError = checkAuthError(hash)
if (!unrecoverableError) {
// Load profile and check session
try {
_profile = await handleSession(hash)
}
catch (err) {
_profile = null
unrecoverableError = err
}
// Hereditas profile (from the profile)
if (_profile) {
// Hereditas profile (from the profile)
_hereditasProfile = _profile[process.env.ID_TOKEN_NAMESPACE] || {}
// Check if we have an app token
if (_hereditasProfile.token) {
try {
// Create a new box and fetch the index
_box = new Box()
// Fetch the index asynchronously and do not wait for completion
_box.fetchIndex()
}
catch (err) {
// eslint-disable-next-line no-console
console.error('Error while requesting box\'s data', err)
}
}
}
}
// Store the profile, hereditasProfile and box into Svelte stores
profile.set(_profile)
hereditasProfile.set(_hereditasProfile)
box.set(_box)
authError.set(unrecoverableError)
// Crete a Svelte app by loading the main view
return new App({
target: document.body
})
})()
export default app
================================================
FILE: app/postcss.config.js
================================================
const path = require('path')
const production = !process.env.ROLLUP_WATCH
module.exports = {
plugins: [
require('postcss-import')(),
require('tailwindcss')(path.resolve(__dirname, 'tailwind.config.js')),
require('autoprefixer'),
...(production ? [require('@fullhuman/postcss-purgecss')({
// Specify the paths to all of the template files in your project
content: [
path.resolve(__dirname, 'main.html'),
path.resolve(__dirname, '**/*.svelte'),
path.resolve(__dirname, '**/*.html'),
],
// Whitelist styles that might be in the content generated from markdown
whitelist: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'ul', 'ol', 'li', 'strong', 'b', 'em', 'i', 'a', 'img', 'pre', 'code', 'hr', 'blockquote'],
// Include any special characters you're using in this regular expression
defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || []
})] : []),
],
}
================================================
FILE: app/robots.txt
================================================
User-agent: *
Disallow: /
================================================
FILE: app/routes.js
================================================
// Import routes
import UnlockView from './views/UnlockView.svelte'
import ListView from './views/ListView.svelte'
import ContentView from './views/ContentView.svelte'
// Map of all routes
export default {
'/': UnlockView,
'/list/*': ListView,
'/list': ListView,
'/content/:element': ContentView
}
================================================
FILE: app/stores.js
================================================
import {writable} from 'svelte/store'
export const pageTitle = writable('Hereditas')
export const profile = writable(null)
export const hereditasProfile = writable(null)
export const box = writable(false)
export const authError = writable(null)
================================================
FILE: app/tailwind.config.js
================================================
module.exports = {
theme: {
extend: {
},
container: {
center: true,
}
},
variants: {
},
plugins: [
],
future: {
removeDeprecatedGapUtilities: true,
},
}
================================================
FILE: app/views/ContentView.svelte
================================================
{#await contentPromise}
Loading...
{:then content}
<nav aria-label="breadcrumb" class="mb-4">
<ol class="list-none p-0 inline-flex">
<li class="flex items-center">
<a href="/list/" use:link>home</a>
<span class="px-1">></span>
</li>
{#each content.path as path, i}
<li class="flex items-center">
<a href="/list/{content.path.slice(0, i + 1).join('/') + '/'}" use:link>{path}</a>
<span class="px-1">></span>
</li>
{/each}
<li class="text-gray-600" aria-current="page">{content.name}</li>
</ol>
</nav>
{#if content.display == 'text'}
<section class="rendered">
<pre class="w-full whitespace-pre-wrap">{content.text}</pre>
</section>
{:else if content.display == 'html'}
<section class="rendered">
{@html content.text}
</section>
{:else if content.display == 'image'}
<img src="{content.blob}" alt="{content.name}" class="img-fluid" />
{:else}
Download: <a href="{content.blob}" download="{content.name}">{content.name}</a>
{/if}
{:catch error}
Error: {error}
{/await}
<script>
// Libs
import {link, replace} from 'svelte-spa-router'
import {onMount} from 'svelte'
// Stores
import {box} from '../stores'
// Props
// Params from the route, which includes the element id
export let params = {}
// Promise that returns the content (note the IIFE)
const contentPromise = (() => {
// Get the name of the element
// The element variable should be an id, a hex string of 24 chars
const elementId = (params && params.element) || ''
if (!elementId || !elementId.match(/^[0-9a-f]{24}$/)) {
return Promise.reject('Invalid content')
}
// Get info on the content
const contents = $box.getContents()
// Get the tag from the index
let element
for (const i in contents) {
if (contents[i].dist == elementId) {
element = contents[i]
break
}
}
if (!element) {
return Promise.reject('Content not found')
}
// Fetch the data
return $box.fetchContent(element)
.then((result) => {
// Build the URL for attachments and images, and get the file name
if (result.display != 'text' && result.display != 'html') {
// Convert data to a Blob
result.blob = URL.createObjectURL(new Blob([result.data], {type: 'octet/stream'}))
delete result.data
}
// Get the path components
result.path = element.path.split('/')
result.name = result.path.pop()
return result
})
.catch((error) => {
// Log the error
// eslint-disable-next-line no-console
console.error(error)
return error
})
})()
// Ensure that we have unlocked the box
onMount(() => {
if (!$box || !$box.isUnlocked()) {
replace('/')
}
})
</script>
================================================
FILE: app/views/ListView.svelte
================================================
<nav aria-label="breadcrumb" class="mb-4">
<ol class="list-none p-0 inline-flex">
{#if list.paths && list.paths.length}
<li class="flex items-center">
<a href="/list/" use:link>home</a>
<span class="px-1">></span>
</li>
{#each list.paths as path, i (path)}
{#if i == (list.paths.length - 1)}
<li class="text-gray-600" aria-current="page">{path}</li>
{:else}
<li class="flex items-center">
<a href="/list/{list.paths.slice(0, i + 1).join('/') + '/'}" use:link>{path}</a>
<span class="px-1">></span>
</li>
{/if}
{/each}
{:else}
<li class="text-gray-600" aria-current="page">home</li>
{/if}
</ol>
</nav>
<div class="bg-white w-full max-w-lg p-4 rounded">
<table class="table-auto w-full">
<thead>
<tr>
<th scope="col">Name</th>
</tr>
</thead>
<tbody>
{#each list.folders as folder (folder)}
<tr class="{(list.i++ % 2) ? 'bg-gray-100' : ''}">
<td class="border px-4 py-2">
<a href="/list/{list.prefix ? list.prefix + '/' : ''}{folder}" use:link>{folder}</a>
</td>
</tr>
{/each}
{#each list.files as file (file.dist)}
<tr class="{(list.i++ % 2) ? 'bg-gray-100' : ''}">
<td class="border px-4 py-2">
<a href="/content/{file.dist}" use:link>{file.name}</a>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<script>
// Libs
import {link, replace} from 'svelte-spa-router'
import {onMount} from 'svelte'
// Stores
import {box} from '../stores'
// Props
// Params from the route, which includes the prefix
export let params = {}
// List of contents
const list = {
files: [],
folders: [],
paths: [],
prefix: '',
i: 0
}
$: {
// Check if we have a prefix (folder)
list.prefix = (params && params.wild) || ''
if (list.prefix.charAt(list.prefix.length - 1) == '/') {
list.prefix = list.prefix.substring(0, list.prefix.length - 1)
}
// Replace %20 with a space
list.prefix = list.prefix.replace(/%20/g, ' ')
// Get the list
const contents = $box.getContents()
const files = []
const folders = []
for (const i in contents) {
// Skip items that don't match the prefix
const el = contents[i]
if (list.prefix && el.path.substring(0, list.prefix.length) !== list.prefix) {
continue
}
// Check if it's a file or folder
const parts = el.path.substring(list.prefix.length)
.split('/') // Split paths
.filter((el) => !!el) // Remove empty values
if (parts.length > 1) {
// This is a folder. Check if it's in the list already
const folder = parts[0]
if (folders.indexOf(folder) < 0) {
folders.push(parts[0])
}
}
else {
files.push({
name: parts[0],
dist: el.dist
})
}
}
list.folders = folders
list.files = files
list.paths = list.prefix ? list.prefix.split('/') : []
list.i = 0
}
// Ensure that we have unlocked the box
onMount(() => {
if (!$box || !$box.isUnlocked()) {
replace('/')
}
})
</script>
================================================
FILE: app/views/UnlockView.svelte
================================================
{#if $profile}
<UserProfile />
{:else}
<RequestAuthentication />
{/if}
<h1 class="my-4">About this page</h1>
<section class="rendered mt-4">
{@html welcome}
</section>
<script>
// Libs
import {onMount} from 'svelte'
import {replace} from 'svelte-spa-router'
// Components
import UserProfile from '../components/UserProfile.svelte'
import RequestAuthentication from '../components/RequestAuthentication.svelte'
// Stores
import {box, profile} from '../stores'
// Props
export const welcome = process.env.WELCOME_MD
// If box has already been unlocked, just go straight to the list
onMount(() => {
if ($box && $box.isUnlocked()) {
replace('/list')
}
})
</script>
================================================
FILE: app/webpack.config.js
================================================
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const SriPlugin = require('webpack-subresource-integrity')
const CopyPlugin = require('copy-webpack-plugin')
const {DefinePlugin} = require('webpack')
const path = require('path')
const fs = require('fs')
const marked = require('marked')
const mode = process.env.NODE_ENV || 'production'
const prod = mode === 'production'
const htmlMinifyOptions = {
collapseWhitespace: true,
conservativeCollapse: true,
removeComments: true,
collapseBooleanAttributes: true,
decodeEntities: true,
html5: true,
keepClosingSlash: false,
processConditionalComments: true,
removeEmptyAttributes: true
}
// Welcome content
let welcomeContent = ''
if (fs.existsSync('welcome.md')) {
let welcomeMarkdown = fs.readFileSync('welcome.md', 'utf8')
// Remove the front matter, if any
if (welcomeMarkdown.startsWith('---')) {
welcomeMarkdown = welcomeMarkdown.replace(/^---$.*^---$/ms, '')
}
welcomeContent = marked(welcomeMarkdown)
}
/**
* Returns a configuration object for webpack
*
* @param {Object} appParams - Params for the application
* @returns {Object} Configuration object for webpack
*/
function webpackConfig(appParams) {
return {
entry: {
hereditas: [path.resolve(__dirname, 'main.js')],
},
resolve: {
mainFields: ['svelte', 'browser', 'module', 'main'],
extensions: ['.mjs', '.js', '.svelte'],
modules: [path.resolve(__dirname, '../node_modules')]
},
resolveLoader: {
modules: [path.resolve(__dirname, '../node_modules')]
},
output: {
path: path.resolve(process.cwd(), appParams.distDir),
filename: '[name].[hash].js',
chunkFilename: '[name].[id].[hash].js',
crossOriginLoading: 'anonymous'
},
module: {
// Do not parse wasm files
noParse: /\.wasm$/,
rules: [
{
test: /\.(svelte)$/,
exclude: [],
use: {
loader: 'svelte-loader',
options: {
emitCss: true,
}
}
},
{
test: /\.css$/,
use: [
'style-loader',
{loader: 'css-loader', options: {importLoaders: 1}},
'postcss-loader',
]
},
{
test: /\.wasm$/,
loaders: ['base64-loader'],
type: 'javascript/auto'
}
]
},
// Fixes for argon2-browser
node: {
__dirname: false,
fs: 'empty',
Buffer: false,
process: false
},
mode,
plugins: [
// Constants
new DefinePlugin({
'process.env.AUTH_ISSUER': JSON.stringify(appParams.authIssuer),
'process.env.AUTH_CLIENT_ID': JSON.stringify(appParams.authClientId),
'process.env.ID_TOKEN_NAMESPACE': JSON.stringify(appParams.idTokenNamespace),
'process.env.KEY_SALT': JSON.stringify(appParams.keySalt.toString('base64')),
'process.env.INDEX_TAG': JSON.stringify(appParams.indexTag.toString('base64')),
'process.env.KEY_DERIVATION_FUNCTION': JSON.stringify(appParams.kdf),
'process.env.PBKDF2_ITERATIONS': JSON.stringify(appParams.pbkdf2Iterations),
'process.env.ARGON2_ITERATIONS': JSON.stringify(appParams.argon2Iterations),
'process.env.ARGON2_MEMORY': JSON.stringify(appParams.argon2Memory),
'process.env.WELCOME_MD': JSON.stringify(welcomeContent)
}),
// Extract CSS
new MiniCssExtractPlugin({
filename: '[name].[hash].css'
}),
// Generate the index.html file
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.resolve(__dirname, 'main.html'),
chunks: ['hereditas'],
minify: prod ? htmlMinifyOptions : false
}),
// Enable subresource integrity check
new SriPlugin({
hashFuncNames: ['sha384'],
enabled: prod,
}),
// Copy files
new CopyPlugin({
patterns: [
{from: path.resolve(__dirname, 'robots.txt'), to: ''},
]
}),
],
devtool: prod ? false : 'source-map',
performance: {
// 400 KB (up from default 250 KB)
maxEntrypointSize: 400000
}
}
}
module.exports = webpackConfig
================================================
FILE: auth0/01-whitelist.js
================================================
function (user, context, callback) {
// Apply this rule only for Hereditas, and bypass it for other apps
context.clientMetadata = context.clientMetadata || {};
if (!context.clientMetadata.hereditas) {
return callback(null, user, context);
}
// List of authorized users
const whitelist = /*%ALL_USERS%*/;
// Access should only be granted to verified users.
if (!user.email || !user.email_verified) {
return callback(new UnauthorizedError('Access denied.'));
}
// Check if the user's email address is whitelisted
const userHasAccess = whitelist.some((email) => email === user.email);
if (!userHasAccess) {
return callback(new UnauthorizedError('Access denied.'));
}
// Continue
callback(null, user, context);
}
================================================
FILE: auth0/02-notify.js
================================================
function (user, context, callback) {
// Apply this rule only for Hereditas, and bypass it for other apps
context.clientMetadata = context.clientMetadata || {};
if (!context.clientMetadata.hereditas) {
return callback(null, user, context);
}
// Skip if there's no webhook
if (!configuration || !configuration.WEBHOOK_URL || configuration.WEBHOOK_URL === '0') {
return callback(null, user, context);
}
// List of owners
const owners = /*%OWNERS%*/;
// Trigger the webhook
const role = (owners.some((email) => email === user.email)) ? 'owner' : 'user';
const body = {
value1: `New Hereditas login on ${(new Date()).toUTCString()}. User: ${user.email} (role: ${role})`,
value2: user.email,
value3: role
};
const fetch = require('node-fetch@2.6.0');
fetch(configuration.WEBHOOK_URL, {
method: 'POST',
body: JSON.stringify(body),
headers: {'Content-Type': 'application/json'}
})
// Ensure the response has a valid status code
.then((response) => {
if (response.ok) {
return callback(null, user, context);
} else {
return Promise.reject('Invalid response status code');
}
})
// Catch errors and fail (fail the login even if the notification fails to send)
.catch((err) => {
console.error(err);
callback(new Error('Error sending the notification'));
});
}
================================================
FILE: auth0/03-wait-logic.js
================================================
function (user, context, callback) {
// Apply this rule only for Hereditas, and bypass it for other apps
context.clientMetadata = context.clientMetadata || {};
if (!context.clientMetadata.hereditas) {
return callback(null, user, context);
}
// List of owners
const owners = /*%OWNERS%*/;
// Get the Auth0 management client
const ManagementClient = require('auth0@2.27.0').ManagementClient;
const management = new ManagementClient({
domain: auth0.domain,
clientId: configuration.AUTH0_CLIENT_ID,
clientSecret: configuration.AUTH0_CLIENT_SECRET
});
// Get metadata
const requestTime = context.clientMetadata.requestTime ? parseInt(context.clientMetadata.requestTime, 10) : 0;
const waitTime = parseInt(context.clientMetadata.waitTime, 10);
// Check if the user is an owner
const isOwner = owners.some((email) => email === user.email);
if (isOwner) {
// Enrich the JWT with the app token
if (context.idToken) {
context.idToken['https://hereditas.app'] = {
role: 'owner',
token: configuration.APP_TOKEN,
requestTime: 0,
waitTime: waitTime
};
}
// Reset the timer if it's running
if (requestTime > 0) {
const params = {client_id: context.clientID};
const data = {client_metadata: {requestTime: '0'}};
management.clients.update(params, data, (err, client) => {
if (err) {
console.log(err);
callback(new Error('Error while updating client_metadata'));
}
else {
// Continue
callback(null, user, context);
}
});
}
else {
// Continue
callback(null, user, context);
}
}
else {
const now = parseInt(Date.now() / 1000, 10);
// For non-owners: first, check if the timer has been started already, and we've reached the wait time
if (requestTime > 0) {
// Enrich the JWT with the app token
if (context.idToken) {
// If the wait time has passed, add the token
const token = ((requestTime + waitTime) < now) ?
configuration.APP_TOKEN :
null;
// Enrich the JWT
context.idToken['https://hereditas.app'] = {
role: 'user',
token: token,
requestTime: requestTime,
waitTime: waitTime
};
}
// Continue
callback(null, user, context);
}
else {
// Start the timer
const params = {client_id: context.clientID};
const data = {client_metadata: {requestTime: now.toString()}};
management.clients.update(params, data, (err, client) => {
if (err) {
console.log(err);
callback(new Error('Error while updating client_metadata'));
}
else {
// Enrich the JWT with the app token
if (context.idToken) {
context.idToken['https://hereditas.app'] = {
role: 'user',
requestTime: now,
waitTime: waitTime
};
}
// Continue
callback(null, user, context);
}
});
}
}
}
================================================
FILE: bin/run
================================================
#!/usr/bin/env node
require('@oclif/command')
.run()
.then(require('@oclif/command/flush'))
.catch(require('@oclif/errors/handle'))
================================================
FILE: bin/run.cmd
================================================
@echo off
node "%~dp0\run" %*
================================================
FILE: cli/commands/auth0/sync.js
================================================
'use strict'
const {Command} = require('@oclif/command')
const Config = require('../../lib/Config')
const Auth0Management = require('../../lib/Auth0Management')
class Auth0SetupCommand extends Command {
async run() {
// Read the config file
const config = new Config('hereditas.json')
try {
await config.load()
}
catch (e) {
this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`)
return this.exit(1)
}
// Initialize the management client
const auth0Management = new Auth0Management(config)
// First step: sync the app on Auth0
const clientId = await auth0Management.syncClient(config.get('auth0.hereditasClientId'))
config.set('auth0.hereditasClientId', clientId)
// Second step: create the rules
const ruleIds = await auth0Management.syncRules(config.get('auth0.rules'))
config.set('auth0.rules', ruleIds)
// Third step: create rule settings
await auth0Management.updateRulesConfigs()
// Save config changes
await config.save()
this.log('Auth0 configuration updated successfully')
}
}
// Command description
Auth0SetupCommand.description = `sync the application and rules in Auth0
Synchronizes the status of the resources configured in Auth0: the client (application), the rules and the rule settings.
`
module.exports = Auth0SetupCommand
================================================
FILE: cli/commands/build.js
================================================
'use strict'
const {Command} = require('@oclif/command')
const Config = require('../lib/Config')
const Builder = require('../lib/Builder')
const {cli} = require('cli-ux')
class BuildCommand extends Command {
async run() {
// Read the config file
const config = new Config('hereditas.json')
try {
await config.load()
}
catch (e) {
this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`)
return this.exit(1)
}
// Make sure that we have an Auth0 client id
const clientId = config.get('auth0.hereditasClientId')
if (!clientId) {
this.error('The Hereditas application hasn\'t been configured on Auth0 yet. Please run `hereditas auth0:sync` first')
return this.exit(1)
}
// Check if we have a passphrase passed as environmental variable (useful for development only)
let passphrase
if (process.env.HEREDITAS_PASSPHRASE) {
passphrase = process.env.HEREDITAS_PASSPHRASE
this.warn('Passphrase set through the HEREDITAS_PASSPHRASE environmental variable; this should be used for development only')
}
else {
// Ask for the user passphrase
passphrase = await cli.prompt('User passphrase', {type: 'mask'})
}
if (!passphrase || passphrase.length < 8) {
this.error('Passphrase needs to be at least 8 characters long')
return this.exit(1)
}
// Timer
const startTime = Date.now()
// Build the project
const builder = new Builder(passphrase, config)
await builder.build()
// Done!
const duration = (Date.now() - startTime) / 1000
if (!builder.hasErrors) {
this.log(`Finished building project in ${config.get('distDir')} (took ${duration} seconds)`)
}
else {
this.error(`Build failed (took ${duration} seconds)`)
}
}
}
// Command description
BuildCommand.description = `build an Hereditas project
Build an Hereditas project in the current working directory.
`
module.exports = BuildCommand
================================================
FILE: cli/commands/init.js
================================================
'use strict'
const {Command, flags} = require('@oclif/command')
const fs = require('fs')
const util = require('util')
const process = require('process')
const path = require('path')
const Config = require('../lib/Config')
const {GenerateToken} = require('../lib/Crypto')
class InitCommand extends Command {
async run() {
const {flags} = this.parse(InitCommand)
// Check if the folder is empty
const files = await util.promisify(fs.readdir)('.')
if (files.length) {
this.error(`Directory ${process.cwd()} isn't empty; aborting`)
return this.exit(1)
}
// Get the relative paths to the folders
const contentDir = path.relative('', flags.content)
const distDir = path.relative('', flags.dist)
// Create the directories
const mkdirPromise = util.promisify(fs.mkdir)
await mkdirPromise(contentDir)
await mkdirPromise(distDir)
// Generate an appToken
const appToken = await GenerateToken(21)
// Create configuration
const config = new Config('hereditas.json')
config.create({
distDir: distDir,
contentDir: contentDir,
auth0: {
domain: flags.auth0Domain,
managementClientId: flags.auth0ClientId,
managementClientSecret: flags.auth0ClientSecret
},
urls: flags.url,
waitTime: 86400,
appToken
})
await config.save()
// Create the welcome.md file
const welcomeContent = `---
# This welcome file is displayed in the box's authentication page.
# It can be used to provide information to visitors about what this Hereditas box is, and how it can be used.
# Note that this file is NOT ENCRYPTED, and it's accessible to the entire world; do not write anything confidential in here.
---
## What is this?
Someone (likely, a loved one) told you to come here in case they suddenly disappeared. This box contains important information about the digital life of the person that shared it with you, for example passwords, digital documents, photos, cryptocurrency wallets, etc.
## How do I use this?
Sign in above using your existing account. You will then need to type the passphrase that you've been given.
Unless you're the owner of this box, you won't immediately have access to its content, but instead you'll have to wait a certain amount of time.
During that time, if the owner signs in here too, they will reset the timer and you will not get access to this box.
## About Hereditas
[Hereditas](https://hereditas.app) is an open source project to generate "fully-trustless" digital legacy boxes.
`
await util.promisify(fs.writeFile)(path.relative('', 'welcome.md'), welcomeContent)
this.log('Project initialized')
}
}
// Command description
InitCommand.description = `initialize a new Hereditas project in the current working directory.
Initialize a new Hereditas project in the current working directory, creating the folders for the content and the generated data, as well as the "hereditas.json" configuration file.
The current working directory needs to be empty, or the command will raise an error.
`
// Usage example
InitCommand.usage = `init \\
--auth0Domain "yourdomain.auth0.com" \\
--auth0ClientId "..." \\
--auth0ClientSecret "..." \\
--url "https://my.testhereditas.app"
`
// Command-line options
InitCommand.flags = {
content: flags.string({
char: 'i',
description: 'path of the directory with the content',
default: 'content'
}),
dist: flags.string({
char: 'o',
description: 'path of the dist directory (where output is saved)',
default: 'dist'
}),
auth0Domain: flags.string({
char: 'd',
description: 'Auth0 domain/tenant (e.g. "myhereditas.auth0.com")',
required: true
}),
auth0ClientId: flags.string({
char: 'c',
description: 'Auth0 client ID for the management app',
required: true
}),
auth0ClientSecret: flags.string({
char: 's',
description: 'Auth0 client secret for the management app',
required: true
}),
url: flags.string({
char: 'u',
description: 'URL where the app is deployed to, used for OAuth callbacks (multiple values supported)',
required: true,
multiple: true
})
}
module.exports = InitCommand
================================================
FILE: cli/commands/pack.js
================================================
'use strict'
const {Command} = require('@oclif/command')
const Config = require('../lib/Config')
const util = require('util')
const path = require('path')
const fs = require('fs')
const {CleanDirectory} = require('../lib/Utils')
const execPromise = util.promisify(require('child_process').exec)
class PackCommand extends Command {
async run() {
// Read the config file
const config = new Config('hereditas.json')
try {
await config.load()
}
catch (e) {
this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`)
return this.exit(1)
}
// Make sure that http://localhost:8080 is allowed as url
let urls = config.get('urls')
if (!urls || urls.indexOf('http://localhost:8080') == -1) {
this.error('Before you can pack a box, the URL `http://localhost:8080` must be allowed. Please run `hereditas url:add -u http://localhost:8080` (and then `hereditas auth0:sync`).')
return this.exit(1)
}
// Check that we have go installed
try {
const {stdout} = await execPromise('go version')
const match = stdout.match(/^go version go1\.([0-9]+)/)
if (!match || !match[0] || !match[1]) {
throw Error('Invalid go interpreter')
}
const goVersion = parseInt(match[1], 10)
if (goVersion < 13) {
throw Error('Go 1.13 or higher is required')
}
}
catch (err) {
this.error('Go 1.13 or higher must be installed for this command to work.')
return this.exit(1)
}
// Ensure that the GOPATH and HOME are defined
if (!process.env.GOPATH || !process.env.HOME) {
this.error('Environmental variables GOPATH and HOME must be defined.')
return this.exit(1)
}
// Check that we have packr2 installed
try {
const {stdout} = await execPromise('packr2 version')
const match = stdout.match(/^v2/)
if (!match || !match[0]) {
throw Error('Invalid packr2 version')
}
}
catch (err) {
this.error('packr2 must be installed for this command to work.\nSee https://github.com/gobuffalo/packr/tree/master/v2')
return this.exit(1)
}
// Check that the Hereditas box is built
const distDir = config.get('distDir')
if (!fs.existsSync(path.join(distDir, '_index'))) {
this.error('This Hereditas box hasn\'t been built yet; please run `hereditas build` first.')
return this.exit(1)
}
// Create a directory for the Go app
// Or clean it if it exists
const packPath = path.relative('', 'pack.tmp')
if (fs.existsSync(packPath)) {
await CleanDirectory(packPath)
}
else {
fs.mkdirSync(packPath)
}
// Copy the Go app's files
['main.go', 'go.mod', 'go.sum'].forEach((file) => {
fs.copyFileSync(
path.resolve(__dirname, '../../pack/' + file),
path.join(packPath, file)
)
})
// Create a symlink to distDir inside the packPath
fs.symlinkSync(path.join('..', distDir), path.join(packPath, 'dist'), 'dir')
// Run packr2
await execPromise('packr2', {
cwd: packPath
})
// Build the Go app for all archs
const archs = {
'linux-amd64': {
GOOS: 'linux',
GOARCH: 'amd64'
},
'linux-386': {
GOOS: 'linux',
GOARCH: '386'
},
'linux-arm64': {
GOOS: 'linux',
GOARCH: 'arm64'
},
'linux-armv7': {
GOOS: 'linux',
GOARCH: 'arm',
GOARM: '7'
},
'macos': {
GOOS: 'darwin',
GOARCH: 'amd64'
},
'win64.exe': {
GOOS: 'windows',
GOARCH: 'amd64'
},
'win32.exe': {
GOOS: 'windows',
GOARCH: '386'
}
}
for (const extension in archs) {
if (!archs.hasOwnProperty(extension)) {
continue
}
const file = 'hereditas-box-' + extension
this.log('Building ' + file)
// Environmental variables
const env = Object.assign({
GOPATH: process.env.GOPATH,
HOME: process.env.HOME,
CGO_ENABLED: '0',
GO111MODULE: 'on'
}, archs[extension])
await execPromise('go build -o ' + path.join('..', '_bin', file), {
cwd: packPath,
env
})
}
// Delete the temporary folder
await CleanDirectory(packPath)
fs.rmdirSync(packPath)
this.log('Done! Binaries are in the _bin folder')
}
}
// Command description
PackCommand.description = `pack a box into a self-contained binary
After building a box with Hereditas, the \`hereditas pack\` command allows you to generate a self-contained binary (for Windows, macOS and Linux) that contains your Hereditas box and all of its contents. Depending on your use case, this single binary might be easier to distribute.
Note that this command has some pre-requisites:
- You need to have the Go compiler installed
(version 1.13 or higher)
- You need to have packr2 installed in your path
(see https://github.com/gobuffalo/packr/tree/master/v2)
- Your Hereditas box must be already built
(run \`hereditas build\` beforehand)
- The URL \`http://localhost:8080\` must be allowed for this box
(run \`hereditas url:add -u http://localhost:8080\`)
`
module.exports = PackCommand
================================================
FILE: cli/commands/regenerate-token.js
================================================
'use strict'
const {Command} = require('@oclif/command')
const Config = require('../lib/Config')
const {GenerateToken} = require('../lib/Crypto')
class RegenerateTokenCommand extends Command {
async run() {
// Read the config file
const config = new Config('hereditas.json')
try {
await config.load()
}
catch (e) {
this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`)
return this.exit(1)
}
// Generate a new appToken and save it
const appToken = await GenerateToken(21)
config.set('appToken', appToken)
await config.save()
this.log('New application token saved in the configuration file')
// Notify users that they need to run the auth0:sync command
this.log('Info: The new application token will be used for boxes you build from now on, using `hereditas build`; it will not impact existing boxes. Additionally, remember to run `hereditas auth0:sync` to update the application token on Auth0 after deploying the new box.')
}
}
// Command description
RegenerateTokenCommand.description = `regenerate the application token
Update the "application token", which is part of the encryption key, in the hereditas.json config file, by generating a new random one.
After running this command, you will need to build a new box with \`hereditas build\` and then synchronize the changes on Auth0 with \`hereditas auth0:sync\`.
`
module.exports = RegenerateTokenCommand
================================================
FILE: cli/commands/url/add.js
================================================
'use strict'
const {Command, flags} = require('@oclif/command')
const Config = require('../../lib/Config')
class UrlAddCommand extends Command {
async run() {
const {flags} = this.parse(UrlAddCommand)
// Read the config file
const config = new Config('hereditas.json')
try {
await config.load()
}
catch (e) {
this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`)
return this.exit(1)
}
// Load the current list
let urls = config.get('urls')
if (!urls) {
urls = []
}
// Add all new URLs
for (let i = 0; i < flags.url.length; i++) {
if (urls.indexOf(flags.url[i]) != -1) {
this.log(`URL ${flags.url[i]} is already present`)
}
else {
// Add url
urls.push(flags.url[i])
this.log(`Added URL ${flags.url[i]}`)
}
}
// Save changes
config.set('urls', urls)
await config.save()
this.log('URL list updated')
// Notify users that they need to run the auth0:sync command
this.log('Info: The configuration has been updated locally; for changes to be effective, remember to run `hereditas auth0:sync`')
}
}
// Command description
UrlAddCommand.description = `add URLs where the box is deployed to, used for OAuth callbacks
Add one or more URLs to the list of addresses where the Hereditas box is deployed to. This information is stored on Auth0 to whitelist URLs where users are redirected after a successful authentication. Note that the protocol (\`http://\` or \`https://\`) needs to match too.
After running this command, you will need to synchronize the changes on Auth0 with \`hereditas auth0:sync\` (it's not necessary to re-build or re-deploy the box).
`
// Usage example
UrlAddCommand.usage = `url:add \\
--url "https://my.testhereditas.app"
`
// Command-line options
UrlAddCommand.flags = {
url: flags.string({
char: 'u',
description: 'URL where the box is deployed to (multiple values supported)',
required: true,
multiple: true
})
}
module.exports = UrlAddCommand
================================================
FILE: cli/commands/url/list.js
================================================
'use strict'
const {Command} = require('@oclif/command')
const Config = require('../../lib/Config')
class UrlListCommand extends Command {
async run() {
// Read the config file
const config = new Config('hereditas.json')
try {
await config.load()
}
catch (e) {
this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`)
return this.exit(1)
}
// Load all urls
let urls = config.get('urls')
if (!urls) {
urls = []
}
this.log(urls.join('\n'))
}
}
// Command description
UrlListCommand.description = `list URLs where the box is deployed to
Shows the list of URLs where the Hereditas box is deployed to. This list is used by Auth0 to whitelist redirect URLs after users authenticate.
`
module.exports = UrlListCommand
================================================
FILE: cli/commands/url/rm.js
================================================
'use strict'
const {Command, flags} = require('@oclif/command')
const Config = require('../../lib/Config')
class UrlRmCommand extends Command {
async run() {
const {flags} = this.parse(UrlRmCommand)
// Read the config file
const config = new Config('hereditas.json')
try {
await config.load()
}
catch (e) {
this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`)
return this.exit(1)
}
// Load the current list
let urls = config.get('urls')
if (!urls) {
urls = []
}
else {
// Remove urls that match
urls = urls.filter((el) => flags.url.indexOf(el) == -1)
}
if (!urls.length) {
this.error('Cannot remove all URLs from the list')
return this.exit(1)
}
// Save changes
config.set('urls', urls)
await config.save()
this.log('URL list updated')
// Notify users that they need to run the auth0:sync command
this.log('Info: The configuration has been updated locally; for changes to be effective, remember to run `hereditas auth0:sync`')
}
}
// Command description
UrlRmCommand.description = `removes URL(s) from the configuration
These URLs are used by Auth0 to whitelist the pages users are redirected to after authenticating.
After running this command, you will need to synchronize the changes on Auth0 with \`hereditas auth0:sync\` (it's not necessary to re-build or re-deploy the box).
`
// Usage example
UrlRmCommand.usage = `url:rm \\
--url "https://my.testhereditas.app"
`
// Command-line options
UrlRmCommand.flags = {
url: flags.string({
char: 'u',
description: 'URL to remove (multiple values supported)',
required: true,
multiple: true
})
}
module.exports = UrlRmCommand
================================================
FILE: cli/commands/user/add.js
================================================
'use strict'
const {Command, flags} = require('@oclif/command')
const Config = require('../../lib/Config')
class UserAddCommand extends Command {
async run() {
const {flags} = this.parse(UserAddCommand)
// Read the config file
const config = new Config('hereditas.json')
try {
await config.load()
}
catch (e) {
this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`)
return this.exit(1)
}
// Check if the user is already in the configuration
let users = config.get('users')
if (!users) {
users = []
}
const added = users.some((el) => el.email == flags.email)
if (added) {
this.log(`User ${flags.email} is already authorized`)
return
}
// Add user
users.push({
email: flags.email,
role: flags.role
})
config.set('users', users)
await config.save()
this.log(`Added user ${flags.email} (role: ${flags.role})`)
// Notify users that they need to run the auth0:sync command
this.log('Info: The configuration has been updated locally; for changes to be effective, remember to run `hereditas auth0:sync`')
}
}
// Command description
UserAddCommand.description = `add an authorized user to the box
Whitelist email addresses to allow users to authenticate and access your Hereditas box. If you configure Auth0 to enable social logins (e.g. Google, Facebook and/or Microsoft accounts), users won't need to set up a new account or password, and they can authenticate with their existing social account as long as the email address matches what you've whitelisted.
When you whitelist an email address, you can choose between the "user" role (the default) and the "owner" one. Someone with the "owner" role can access the data in this Hereditas box at any time (provided they have the "user passphrase" too), and when they authenticate, they reset any timer that might have been started by another person with the "user" role.
After running this command, you will need to synchronize the changes on Auth0 with \`hereditas auth0:sync\` (it's not necessary to re-build or re-deploy the box).
`
// Usage example
UserAddCommand.usage = `user:add \\
--email "someone@example.com"
`
// Command-line options
UserAddCommand.flags = {
email: flags.string({
char: 'e',
description: 'email address of the user to whitelist',
required: true
}),
role: flags.string({
char: 'r',
options: ['user', 'owner'],
description: 'role: user or owner',
default: 'user'
})
}
module.exports = UserAddCommand
================================================
FILE: cli/commands/user/list.js
================================================
'use strict'
const {Command, flags} = require('@oclif/command')
const Config = require('../../lib/Config')
class UserListCommand extends Command {
async run() {
const {flags} = this.parse(UserListCommand)
// Read the config file
const config = new Config('hereditas.json')
try {
await config.load()
}
catch (e) {
this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`)
return this.exit(1)
}
// Load all users
let users = config.get('users')
if (!users) {
users = []
}
const list = {owners: [], users: []}
for (let i = 0; i < users.length; i++) {
if (users[i].role == 'owner') {
list.owners.push(users[i].email)
}
else {
list.users.push(users[i].email)
}
}
list.owners.sort()
list.users.sort()
// Show list
if (!flags.role) {
this.log(`\x1b[1mOwners:\x1b[0m\n ${list.owners.join('\n ')}`)
this.log(`\x1b[1mUsers:\x1b[0m\n ${list.users.join('\n ')}`)
}
else {
this.log(list[flags.role + 's'].join('\n'))
}
}
}
// Command description
UserListCommand.description = `list users that are authorized to authenticate with this box
Prints the list of authorized users (email adddresses) and their role.
`
// Command-line options
UserListCommand.flags = {
role: flags.string({
char: 'r',
options: ['', 'user', 'owner'],
description: 'filter by role: user or owner (or none)',
default: ''
})
}
module.exports = UserListCommand
================================================
FILE: cli/commands/user/rm.js
================================================
'use strict'
const {Command, flags} = require('@oclif/command')
const Config = require('../../lib/Config')
class UserRmCommand extends Command {
async run() {
const {flags} = this.parse(UserRmCommand)
// Read the config file
const config = new Config('hereditas.json')
try {
await config.load()
}
catch (e) {
this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`)
return this.exit(1)
}
// Load users and remove the requested one
let users = config.get('users')
if (!users) {
users = []
}
else {
users = users.filter((el) => el.email != flags.email)
}
// Save
config.set('users', users)
await config.save()
this.log(`Removed user ${flags.email}`)
// Notify users that they need to run the auth0:sync command
this.log('Info: The configuration has been updated locally; for changes to be effective, remember to run `hereditas auth0:sync`')
}
}
// Command description
UserRmCommand.description = `remove an authorized user from this box
Removes an email address from the list of those authorized to authenticate with Auth0 for this Hereditas box.
After running this command, you will need to synchronize the changes on Auth0 with \`hereditas auth0:sync\` (it's not necessary to re-build or re-deploy the box).
`
// Usage example
UserRmCommand.usage = `user:rm \\
--email "someone@example.com"
`
// Command-line options
UserRmCommand.flags = {
email: flags.string({
char: 'e',
description: 'email address of the user to remove from the whitelist',
required: true
})
}
module.exports = UserRmCommand
================================================
FILE: cli/commands/wait-time/get.js
================================================
'use strict'
const {Command} = require('@oclif/command')
const Config = require('../../lib/Config')
class WaitTimeGetCommand extends Command {
async run() {
// Read the config file
const config = new Config('hereditas.json')
try {
await config.load()
}
catch (e) {
this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`)
return this.exit(1)
}
this.log(config.get('waitTime') + 's')
}
}
// Command description
WaitTimeGetCommand.description = `get the current value for the wait time
This command returns the current value for the wait time, in seconds.
The wait time is the amount of time for normal users (that don't have the "owner" role) before they can unlock the Hereditas box. Auth0 will not provide users with the "application token" unless the wait time has passed since their first login, preventing them from having the information required to unlock the Hereditas box. If users with the "owner" role authenticate, the timer is automatically stopped.
`
module.exports = WaitTimeGetCommand
================================================
FILE: cli/commands/wait-time/set.js
================================================
'use strict'
const {Command, flags} = require('@oclif/command')
const Config = require('../../lib/Config')
class WaitTimeSetCommand extends Command {
async run() {
const {flags} = this.parse(WaitTimeSetCommand)
// Read the config file
const config = new Config('hereditas.json')
try {
await config.load()
}
catch (e) {
this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`)
return this.exit(1)
}
// Get the new value
const time = parseInt(flags.time || '0', 10)
if (time < 1) {
this.error('Wait time must be a number greater than zero')
return this.exit(1)
}
// Save changes
config.set('waitTime', time)
await config.save()
this.log('Wait time updated')
// Notify users that they need to run the auth0:sync command
this.log('Info: The configuration has been updated locally; for changes to be effective, remember to run `hereditas auth0:sync`')
}
}
// Command description
WaitTimeSetCommand.description = `configure the wait time
This command sets the wait time (in seconds) for this Hereditas box.
The wait time is the amount of time for normal users (that don't have the "owner" role) before they can unlock the Hereditas box. Auth0 will not provide users with the "application token" unless the wait time has passed since their first login, preventing them from having the information required to unlock the Hereditas box. If users with the "owner" role authenticate, the timer is automatically stopped.
After running this command, you will need to synchronize the changes on Auth0 with \`hereditas auth0:sync\` (it's not necessary to re-build or re-deploy the box).
`
// Usage example
WaitTimeSetCommand.usage = `wait-time:set \\
--time 86400
`
// Command-line options
WaitTimeSetCommand.flags = {
time: flags.string({
char: 't',
description: 'wait time, in seconds',
required: true,
})
}
module.exports = WaitTimeSetCommand
================================================
FILE: cli/commands/webhook/get.js
================================================
'use strict'
const {Command} = require('@oclif/command')
const Config = require('../../lib/Config')
class WebhookGetCommand extends Command {
async run() {
// Read the config file
const config = new Config('hereditas.json')
try {
await config.load()
}
catch (e) {
this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`)
return this.exit(1)
}
this.log(config.get('webhookUrl') || 'No webhook configured')
}
}
// Command description
WebhookGetCommand.description = `get the current value for the webhook URL
Hereditas configures Auth0 to send a notification when someone successfully authenticates into this Hereditas box. The notification can be used as a warning that the timer has started.
Notifications are sent by invoking a webhook, which can then trigger any action you desire. See the Hereditas documentation for examples and ideas on how to use this feature.
If no webhook is set, Hereditas will not send you notifications on new logins.
`
module.exports = WebhookGetCommand
================================================
FILE: cli/commands/webhook/set.js
================================================
'use strict'
const {Command, flags} = require('@oclif/command')
const Config = require('../../lib/Config')
class WebhookSetCommand extends Command {
async run() {
const {flags} = this.parse(WebhookSetCommand)
// Read the config file
const config = new Config('hereditas.json')
try {
await config.load()
}
catch (e) {
this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`)
return this.exit(1)
}
// Save changes
config.set('webhookUrl', (flags.url === 'none') ? undefined : flags.url)
await config.save()
this.log('Webhook URL updated')
// Notify users that they need to run the auth0:sync command
this.log('Info: The configuration has been updated locally; for changes to be effective, remember to run `hereditas auth0:sync`')
}
}
// Command description
WebhookSetCommand.description = `set the webhook URL used to notify of new logins
Hereditas configures Auth0 to send a notification when someone successfully authenticates into this Hereditas box. The notification can be used as a warning that the timer has started.
Notifications are sent by invoking a webhook, which can then trigger any action you desire. See the Hereditas documentation for examples and ideas on how to use this feature.
You can disable notifications by setting \`--url none\` when invoking this command.
After running this command, you will need to synchronize the changes on Auth0 with \`hereditas auth0:sync\` (it's not necessary to re-build or re-deploy the box).
`
// Usage example
WebhookSetCommand.usage = `webhook:set \\
--url "https://example.com/webhook/token/abc123XYZ"
`
// Command-line options
WebhookSetCommand.flags = {
url: flags.string({
char: 'u',
description: 'webhook URL; set to "none" to remove',
required: true,
})
}
module.exports = WebhookSetCommand
================================================
FILE: cli/index.js
================================================
module.exports = require('@oclif/command')
================================================
FILE: cli/lib/Auth0Management.js
================================================
'use strict'
const fs = require('fs')
const util = require('util')
const path = require('path')
const ManagementClient = require('auth0').ManagementClient
/**
* Configures Auth0 to work with Hereditas
*/
class Auth0Management {
/**
* Initializes the object
* @param {Config} config - Config object
*/
constructor(config) {
// Ensure that the configuration has Auth0 credentials
const auth0Config = config.get('auth0')
if (!auth0Config || !auth0Config.domain || !auth0Config.managementClientId || !auth0Config.managementClientSecret) {
throw Error('Auth0 Management Client credentials are not present')
}
this._config = config
this._management = new ManagementClient({
domain: auth0Config.domain,
clientId: auth0Config.managementClientId,
clientSecret: auth0Config.managementClientSecret
})
}
/**
* Ensures that we have a client (application) on Auth0 whose configuration matches the desired one. If a client ID is passed, the method checks if the client exists and updates it; otherwise, it will create a new client.
*
* @param {string} [clientId] - Auth0 client (application) ID
* @returns {string} Client ID of the application (either new or updated)
*/
async syncClient(clientId) {
// Check if we already have a client and it exists
if (clientId) {
// Check if it exists; if it does, update the data
let data
try {
data = await this.getClient(clientId)
}
catch (err) {
// If the exception is because the client doesn't exist, all good; otherwise, re-throw it
if (err.toString().match(/Not Found/i)) {
data = null
}
else if (err.name && err.name == 'access_denied') {
throw Error('Invalid Auth0 credentials')
}
else {
throw err
}
}
// If we have an existing client, update it
if (data) {
try {
await this.updateClient(clientId)
}
catch (err) {
if (err.name && err.name == 'access_denied') {
throw Error('Invalid Auth0 credentials')
}
else {
throw err
}
}
}
else {
clientId = undefined
}
}
// If client doesn't exist, create it
if (!clientId) {
try {
clientId = await this.createClient()
}
catch (err) {
if (err.name && err.name == 'access_denied') {
throw Error('Invalid Auth0 credentials')
}
else {
throw err
}
}
}
return clientId
}
/**
* Updates a client (application) on Auth0 so the configuration matches the desired one.
*
* @param {string} clientId - Auth0 client (application) ID
* @returns {string} Client ID of the updated application
*/
async updateClient(clientId) {
const params = {
client_id: clientId
}
const result = await this._management.clients.update(params, this._clientConfiguration())
if (result) {
return result.client_id
}
}
/**
* Create the client (application) on Auth0.
*
* @returns {string} Client ID of the new application
*/
async createClient() {
// Create the client
const result = await this._management.clients.create(this._clientConfiguration())
if (result) {
return result.client_id
}
}
/**
* Retrieve a client (application) from Auth0.
*
* @param {string} clientId - Client ID
*/
async getClient(clientId) {
const data = await this._management.clients.get({client_id: clientId})
if (!data || data.client_id != clientId) {
return null
}
return data
}
/**
* Ensures that the rules Hereditas needs are present in Auth0, and re-creates them so they're on the last version of the configuration and code.
*
* @param {Array<string>} [ruleIds] - List of rules already created by Hereditas (if any)
* @returns {Array<string>} New list of rules managed by Hereditas
*/
async syncRules(ruleIds) {
// First, check if the rules still exist
if (ruleIds && ruleIds.length) {
const rules = await this.listRules()
if (rules && Array.isArray(rules) && rules.length) {
// Delete all rules from the array that still exist
const promises = []
for (let i = 0; i < rules.length; i++) {
const el = rules[i]
if (!el || !el.id) {
continue
}
if (ruleIds.indexOf(el.id) != -1) {
promises.push(this._management.rules.delete({id: el.id}))
}
}
// Await all requests in parallel
await Promise.all(promises)
}
}
// Lastly, re-create all rules and return the new IDs
return this.createRules()
}
/**
* List all rules
*
* @returns {Array} List of rules
* @async
*/
listRules() {
return this._management.rules.getAll()
}
/**
* Create all rules required by Hereditas.
*
* @returns {Array<string>} Array with the ID of the rules, in order
* @async
*/
async createRules() {
// Read all scripts
const readFilePromise = util.promisify(fs.readFile)
const scripts = await Promise.all([
readFilePromise(path.resolve(__dirname, '../../auth0/01-whitelist.js'), 'utf8'),
readFilePromise(path.resolve(__dirname, '../../auth0/02-notify.js'), 'utf8'),
readFilePromise(path.resolve(__dirname, '../../auth0/03-wait-logic.js'), 'utf8')
])
const names = [
'Hereditas 01 - Whitelist email addresses',
'Hereditas 02 - Notify',
'Hereditas 03 - Wait logic'
]
// Replacer function in scripts
const users = this._config.get('users') || []
const replacer = (script) => {
const vars = {
'/*%ALL_USERS%*/': JSON.stringify(users.map((el) => el.email)),
'/*%OWNERS%*/': JSON.stringify(users.filter((el) => el.role == 'owner').map((el) => el.email))
}
return script.replace(/\/\*%([A-Za-z0-9_]+)%\*\//, (token) => {
return vars[token]
})
}
// Create all rules, in order
const promises = []
for (let i = 0; i < 3; i++) {
promises.push(this._management.rules.create({
enabled: true,
stage: 'login_success',
order: i + 1,
name: names[i],
script: replacer(scripts[i])
}))
}
const results = await Promise.all(promises)
// Return the IDs of the rules
return results.map((el) => el.id)
}
/**
* List all rules configurations (only the keys, not values)
*
* @returns {Array} Array with all the rules configurations
* @async
*/
listRulesConfigs() {
return this._management.rulesConfigs.getAll()
}
/**
* Updates all rules configurations. This creates new configurations, and overwrites existing ones.
*
* @async
*/
async updateRulesConfigs() {
const rulesConfigs = {
APP_TOKEN: this._config.get('appToken'),
AUTH0_CLIENT_ID: this._config.get('auth0.managementClientId'),
AUTH0_CLIENT_SECRET: this._config.get('auth0.managementClientSecret'),
WEBHOOK_URL: this._config.get('webhookUrl') || '0'
}
// Create all rules configurations
const promises = []
for (const key in rulesConfigs) {
const value = rulesConfigs[key]
promises.push(this._management.rulesConfigs.set({key}, {value}))
}
await Promise.all(promises)
}
/**
* Returns the configuration object for a client (application) on Auth0.
*
* @returns {Object} Configuration object for the client (application) on Auth0
*/
_clientConfiguration() {
return {
name: 'Hereditas',
is_first_party: true,
oidc_conformant: true,
cross_origin_auth: false,
description: 'This application is managed by the Hereditas CLI. For information, see https://hereditas.app',
logo_uri: '',
sso: false,
callbacks: this._config.get('urls'),
allowed_logout_urls: [],
allowed_clients: [],
client_metadata: {
requestTime: '0',
waitTime: this._config.get('waitTime') + '', // Cast as string
hereditas: '1'
},
allowed_origins: [],
jwt_configuration: {
alg: 'RS256',
lifetime_in_seconds: 1800
},
token_endpoint_auth_method: 'none',
app_type: 'spa',
grant_types: [
'implicit'
]
}
}
}
module.exports = Auth0Management
================================================
FILE: cli/lib/Builder.js
================================================
'use strict'
const fs = require('fs')
const crypto = require('crypto')
const {Readable} = require('stream')
const util = require('util')
const Content = require('./Content')
const {CleanDirectory} = require('./Utils')
const path = require('path')
const kw = require('./aes-kw')
const argon2 = require('argon2-browser')
// Webpack
const webpack = util.promisify(require('webpack'))
const webpackConfig = require('../../app/webpack.config')
// Promisified fs.readdir, fs.stat and fs.unlink
const readdirPromise = util.promisify(fs.readdir)
const statPromise = util.promisify(fs.stat)
// Promisified crypto.pbkdf2 and crypto.randomBytes
const pbkdf2Promise = util.promisify(crypto.pbkdf2)
const randomBytesPromise = util.promisify(crypto.randomBytes)
/**
* Object containing properties for a file in the content directory
*
* @typedef {Object} HereditasContentFile
* @property {string} path - Path of the file (relative to the contentDir)
* @property {number} size - File size in bytes
* @property {string} dist - Random filename used in the dist folder
* @property {string} tag - Authentication tag for AES-GCM
* @property {string} processed - If the file has been pre-processed, this explains how (e.g. "markdown"); it's undefined otherwise
* @property {"text"|"image"|"attach"} display - Configures how the file should be displayed
*/
/**
* Builds a project
*/
class Builder {
/**
* Initializes the object
* @param {string} passphrase - User passphrase
* @param {Config} config - Config object
*/
constructor(passphrase, config) {
// Store config in the object
this._config = config
this._passphrase = passphrase
// Output
this.keySalt = null
this.indexTag = null
this.hasErrors = false
}
/**
* Performs a full build
*
* @async
*/
async build() {
// Step 1: clean dist directory
await CleanDirectory(this._config.get('distDir'))
// Step 2: get the list of files
let content = await this._scanContent()
// Step 3: generate a salt for deriving the encryption key
// This needs to be of 64 bytes, which is the length of a SHA-512 hash
this.keySalt = await randomBytesPromise(64)
// Step 4: derive the master key
const masterKey = await this._deriveKey(this._passphrase + this._config.get('appToken'), this.keySalt)
// Step 5: encrypt all files
content = await this._encryptContent(masterKey, content)
// Step 6: write an (encrypted) index file
this.indexTag = await this._createIndex(masterKey, content)
// Step 7: build the app with webpack
const appParams = {
distDir: this._config.get('distDir'),
authIssuer: 'https://' + this._config.get('auth0.domain'),
authClientId: this._config.get('auth0.hereditasClientId'),
idTokenNamespace: 'https://hereditas.app',
indexTag: this.indexTag,
keySalt: this.keySalt,
kdf: this._config.get('kdf'),
pbkdf2Iterations: this._config.get('pbkdf2.iterations'),
argon2Iterations: this._config.get('argon2.iterations'),
argon2Memory: this._config.get('argon2.memory')
}
const webpackStats = await webpack(webpackConfig(appParams))
// Check if webpack compilation had errors
if (webpackStats.hasErrors()) {
const errors = webpackStats.toJson().errors
// eslint-disable-next-line no-console
console.error('\x1b[31m\x1b[1m' + 'WEBPACK ERRORS' + '\x1b[0m\n')
for (const i in errors) {
// eslint-disable-next-line no-console
console.error('\x1b[31m' + errors[i] + '\x1b[0m\n')
}
this.hasErrors = true
}
if (webpackStats.hasWarnings()) {
const warnings = webpackStats.toJson().warnings
// eslint-disable-next-line no-console
console.warn('\x1b[33m\x1b[1m' + 'WEBPACK WARNINGS' + '\x1b[0m\n')
for (const i in warnings) {
// eslint-disable-next-line no-console
console.warn('\x1b[33m' + warnings[i] + '\x1b[0m\n')
}
}
}
/**
* Derives a 256 bit key from the passphrase and the salt, using the preferred key derivation function.
* The key can be used directly for symmetric encryption.
*
* @param {string} passphrase - Passphrase for the key
* @param {Buffer} salt - Salt for the key
* @returns {Promise<Buffer>} Promise that resolves to the buffer with the key
* @async
*/
_deriveKey(passphrase, salt) {
const kdf = this._config.get('kdf')
if (kdf == 'pbkdf2') {
// Using SHA-512, the result is a 512 bit key, so truncate it to 256 bit (32 bytes)
return pbkdf2Promise(
passphrase,
salt,
this._config.get('pbkdf2.iterations'),
32,
'sha512'
)
}
else if (kdf == 'argon2') {
return Promise.resolve()
.then(() => argon2.hash({
pass: passphrase,
salt: salt,
type: argon2.ArgonType.Argon2id,
time: this._config.get('argon2.iterations'),
mem: this._config.get('argon2.memory'),
hashLen: 32,
parallelism: 1
}))
.then((res) => {
return Buffer.from(res.hash)
})
}
else {
throw Error('Invalid key derivation function requested')
}
}
/**
* Creates an index file and encrypts it on disk.
*
* @param {Buffer} masterKey - Master encryption key
* @param {HereditasContentFile[]} content - List of content
* @returns {Buffer} Authentication tag
* @async
*/
async _createIndex(masterKey, content) {
// Creat the index file, and convert it to a Readable Stream
const indexData = JSON.stringify(content)
const inStream = new Readable()
inStream._read = () => {} // _read is required, but it's a no-op
inStream.push(indexData, 'utf8')
inStream.push(null) // End
// Output stream
const outStream = fs.createWriteStream(path.join(this._config.get('distDir'), '_index'))
// Encrypt the index and write it, returning the tag
return this._encryptStream(masterKey, inStream, outStream)
}
/**
* Encrypts all the content
* @param {Buffer} masterKey - Master encryption key
* @param {HereditasContentFile[]} content - List of content
* @returns {HereditasContentFile[]} - List of content with the dist and tag properties set
* @async
*/
async _encryptContent(masterKey, content) {
// Clone the content object
const result = JSON.parse(JSON.stringify(content))
// Iterate through the content and encrypt each file
for (const i in result) {
// Generate the file name for the output file (a random hex string)
const dist = (await randomBytesPromise(12)).toString('hex')
// Create the Readable stream to the input, and Writable stream to the output
const outStream = fs.createWriteStream(path.join(this._config.get('distDir'), dist))
// Pre-process the file
const content = new Content(result[i], this._config)
await content.process()
result[i] = content.el
// Encrypt the stream and get the tag
const tagBuf = await this._encryptStream(masterKey, content.inStream, outStream)
const tag = tagBuf.toString('base64')
// Add the dist and tag properties to the result object
result[i].dist = dist
result[i].tag = tag
}
return result
}
/**
* Encrypts a stream using aes-256-gcm
*
* @param {Buffer} masterKey - Master key; must be 256 bit long
* @param {Stream} inStream - Readable stream with the data to encrypt
* @param {Stream} outStream - Writable stream to pipe the data to
* @returns {Buffer} Authentication tag
* @async
*/
async _encryptStream(masterKey, inStream, outStream) {
// Generate a key for this specific file
const fileKey = await randomBytesPromise(32)
// Generate an IV
const fileIV = await randomBytesPromise(12)
// Wrap the file's key with the master key, using AES-KW (RFC-3394)
const wrappedKey = kw.encrypt(masterKey, fileKey)
return new Promise((resolve, reject) => {
// Write the wrapped key and IV to the outStream, at the beginning
outStream.write(wrappedKey)
outStream.write(fileIV)
// Create the Cipher, which can be used as a stream transform too
const cipher = crypto.createCipheriv('aes-256-gcm', fileKey, fileIV)
// When the encryption is done, get the authentication tag
cipher.on('end', () => {
resolve(cipher.getAuthTag())
})
// In case of errors, throw
inStream.on('error', reject)
outStream.on('error', reject)
// Pipe the input stream through the cipher and then to the output stream
inStream.pipe(cipher).pipe(outStream)
})
}
/**
* Recursively scans the content directory, listing files
* @returns {HereditasContentFile[]} List of files
* @async
*/
async _scanContent() {
// Will contain the final list
const result = []
// Recursive function that scans folders
const scanFolder = async (folder) => {
folder = folder || ''
// Scan the list of files and folders, recursively
const list = await readdirPromise(path.join(this._config.get('contentDir'), folder))
for (const e in list) {
const el = folder + list[e]
// Check if we need to include this path or ignore it
if (!includePath(el)) {
continue
}
// Check if it's a directory
const stat = await statPromise(path.join(this._config.get('contentDir'), el))
if (!stat) {
continue
}
// If it's a directory, scan it recursively
if (stat.isDirectory()) {
await scanFolder(el + path.sep)
}
else {
// Add the file to the list
result.push({
path: el,
size: stat.size
})
}
}
}
// Get the list
await scanFolder()
return result
}
}
// Returns true if a path should be included in the box
// This ignores files such as operating system's metadata
function includePath(str) {
const base = path.basename(str)
if (
// Linux
base.endsWith('~') ||
base == '.directory' ||
// macOS
base == '.DS_Store' ||
base == '.AppleDouble' ||
base == '.LSOverride' ||
base.startsWith('._') ||
// Windows
base == 'Thumbs.db' ||
base == 'Thumbs.db:encryptable' ||
base == 'desktop.ini' ||
base == 'Desktop.ini'
) {
return false
}
return true
}
module.exports = Builder
================================================
FILE: cli/lib/Config.js
================================================
'use strict'
const fs = require('fs')
const util = require('util')
const defaultsDeep = require('lodash.defaultsdeep')
const cloneDeep = require('lodash.clonedeep')
const SMHelper = require('smhelper')
const ConfigVersion = 20190222
/**
* Authorized users
*
* @typedef {object} HereditasUser
* @property {string} email - Email address
* @property {"user"|"owner"} role - Role: "user" or "owner"
*/
/**
* Configuration dictionary for Hereditas
*
* @typedef {object} HereditasConfig
* @property {number} version - Version of the configuration object
* @property {string} contentDir - Folder containing the source content
* @property {string} distDir - Folder where to place the compiled project
* @property {boolean} processMarkdown - If true, enable processing of Markdown files into HTML
* @property {object} auth0 - Auth0 configuration
* @property {string} auth0.domain - Auth0 domain/tenant (e.g. "myhereditas.auth0.com")
* @property {string} auth0.hereditasClientId - Auth0 app client ID for Hereditas
* @property {string} auth0.managementClientId - Client ID for the Auth0 Management app
* @property {string} auth0.managementClientSecret - Client Secret for the Auth0 Management app
* @property {Array<string>} rules - ID of the Auth0 rules created by the Hereditas CLI
* @property {"pbkdf2"|"argon2"} kdf - Key derivation function to use: pbkdf2 or argon2 (default)
* @property {object} pbkdf2 - Configuration for pbkdf2
* @property {string} pbkdf2.iterations - Number of iterations
* @property {string} webhookUrl - URL of the webhook to trigger when a new user logs into Hereditas.
* @property {Array<HereditasUser>} users - List of users
* @property {string} appToken - Application token; when combined with the user passphrase, this allows deriving the encryption key
* @property {number} waitTime - The amount of time, in seconds, to wait before Auth0 can return to users the app token
* @property {Array<string>} urls - list of URLs where your app will be deployed to, e.g. `https://hereditas.example.com`, or `https://myname.blob.core.windows.net/hereditas`, etc; this is used for OAuth redirects.
*/
/**
* Helper class for managing Hereditas configuration
*/
class Config {
/**
* Initializes the object.
*
* @param {string} [filename="hereditas.json"] - Name of the file on disk
*/
constructor(filename) {
if (!filename) {
filename = 'hereditas.json'
}
this._filename = filename
// userConfig is the data read from the config file. config is that, plus defaults
this._userConfig = null
this._config = {}
}
/**
* Create a new userConfig object.
*
* Note that this doesn't save changes on disk, you must manually call `save()`.
*
* @param {HereditasConfig} initConfig - Initial configuration values
*/
create(initConfig) {
this._userConfig = {
version: ConfigVersion
}
defaultsDeep(this._userConfig, initConfig)
// Update the config in memory
this._config = {}
this._defaults()
}
/**
* Reads and parses a config file, validating it.
*
* @param {string} [filename="hereditas.json"] - Name of the config file to read; default is "hereditas.json"
* @throws Throws an error if the config file doesn't exist or is not a valid Hereditas config
*/
async load() {
// Read the file
const configFile = await util.promisify(fs.readFile)(this._filename, 'utf8')
if (!configFile) {
throw Error('Cannot read config file')
}
// Parse JSON and ensure it's a valid format
this._userConfig = JSON.parse(configFile)
if (!this._validate()) {
throw Error('Invalid config file')
}
// Apply defaults
this._defaults()
}
/**
* Returns value for key from configuration
*
* @param {string} key - Key of the object, in "dot notation"
* @returns {*} Value of the configuration key requested (cloned)
*/
get(key) {
let val
// If key contains a dot, we are requesting a nested object
if (key.indexOf('.') != -1) {
val = SMHelper.getDescendantProperty(this._config, key)
}
else {
val = this._config[key]
}
// Returns a clone of the object so it can't be modified
return cloneDeep(val)
}
/**
* Returns all config values (cloned).
*
* @returns {HereditasConfig} All configuration data
*/
all() {
return cloneDeep(this._config)
}
/**
* Updates the value of a user config.
*
* Note: this does NOT save the changes on disk; you must invoke `save()` for that.
*
* @param {string} key - Name of the key to update, using the "dot notation"
* @param {*} value - New value
*/
set(key, value) {
// Update the value and validate the config
SMHelper.updatePropertyInObject(this._userConfig, key, value)
if (!this._validate()) {
throw Error('Invalid config data')
}
// Update the config in memory
this._config = {}
this._defaults()
}
/**
* Save changes to user configuration to disk.
*
* @returns {Promise} Returns a promise that resolves when the changes have been committed to disk.
* @async
*/
save() {
return util.promisify(fs.writeFile)(this._filename, JSON.stringify(this._userConfig, null, 2))
}
/**
* Validates a config object, ensuring that all required keys are present.
*
* @returns {boolean} Returns true on valid configuration objects
* @throws Throws an Error if the config file isn't valid
*/
_validate() {
if (!this._userConfig || typeof this._userConfig != 'object' || !Object.keys(this._userConfig).length) {
throw Error('Invalid config file')
}
if (!this._userConfig.version) {
throw Error('Config file is missing required key version')
}
if (!this._userConfig.distDir) {
throw Error('Config file is missing required key distDir')
}
if (!this._userConfig.contentDir) {
throw Error('Config file is missing required key contentDir')
}
if (!this._userConfig.appToken) {
throw Error('Config file is missing required key appToken')
}
if (!this._userConfig.auth0 || typeof this._userConfig.auth0 != 'object' || !Object.keys(this._userConfig.auth0).length) {
throw Error('Config file is missing required key auth0')
}
if (!this._userConfig.auth0.domain) {
throw Error('Config file is missing required key auth0.domain')
}
if (!this._userConfig.urls || !Array.isArray(this._userConfig.urls) || !this._userConfig.urls.length) {
throw Error('Config file is missing required key urls')
}
return true
}
/**
* Applies default parameters to the userConfig object, and stores that into the config object
*/
_defaults() {
defaultsDeep(this._config, this._userConfig, {
processMarkdown: true,
kdf: 'argon2',
pbkdf2: {
iterations: 100000
},
argon2: {
iterations: 2,
memory: 64 * 1024
}
})
}
}
module.exports = Config
================================================
FILE: cli/lib/Content.js
================================================
'use strict'
const fs = require('fs')
const {Readable} = require('stream')
const util = require('util')
const path = require('path')
// Marked.js
const marked = util.promisify(require('marked'))
// Promisified fs.readFile, fs.readdir, fs.stat and fs.unlink
const readFilePromise = util.promisify(fs.readFile)
// List of file extensions of images
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp']
/**
* Processes content
*/
class Content {
/**
* Constructor
*
* @param {HereditasContentFile} el - Content to process
* @param {Config} config - Config object
*/
constructor(el, config) {
this._config = config
this._el = el
this._inStream = null
}
/**
* Object with the content information, potentially modified
*
* @returns {HereditasContentFile} Content information object
*/
get el() {
return this._el
}
/**
* Readable stream to the (optionally, pre-processed) content
*
* @returns {ReadableStream} Stream with content data
*/
get inStream() {
return this._inStream
}
/**
* Pre-processes the content in any way necessary, e.g. converting Markdown into HTML.
*/
async process() {
if (this._el.path.match(/\.txt$/i)) {
await this._processText()
}
else if (this._el.path.match(/\.(md|markdown)$/i)) {
await this._processMarkdown()
}
else {
await this._processBinary()
}
}
/**
* Processes images and other binary files
*/
async _processBinary() {
// Just get a stream to the file on disk
this._inStream = fs.createReadStream(path.join(this._config.get('contentDir'), this._el.path))
// Set the display as "image" for images, and "attach" for anything else
const extension = this._el.path.split('.')
.pop()
.toLowerCase()
this._el.display = (imageExtensions.indexOf(extension) < 0) ?
'attach' :
'image'
}
/**
* Processes simple Text files
*/
async _processText() {
// Get a stream to the file and display it as text
this._inStream = fs.createReadStream(path.join(this._config.get('contentDir'), this._el.path))
this._el.display = 'text'
}
/**
* Processes Markdown files, converting them to HTML
*/
async _processMarkdown() {
// Check if we process Markdown into HTML
if (this._config.get('processMarkdown')) {
const markdown = await readFilePromise(path.join(this._config.get('contentDir'), this._el.path), 'utf8')
const html = await marked(markdown)
// Push the data into a stream
this._inStream = new Readable()
this._inStream._read = () => {} // _read is required, but it's a no-op
this._inStream.push(html, 'utf8')
this._inStream.push(null) // End
// Mark the file as pre-processed
this._el.processed = 'markdown'
this._el.display = 'html'
// TODO: Handle different encodings
}
else {
// If not processing them, treat Markdown files as simple text
await this._processText()
}
}
}
module.exports = Content
================================================
FILE: cli/lib/Crypto.js
================================================
'use strict'
const crypto = require('crypto')
const util = require('util')
/**
* Generates a token with `length` random bytes, and returns it as a base64-encoded string.
*
* @param {number} length - Number of bytes to generate (before converting to base64)
* @returns {string} Token represented as base64-encoded string
*/
async function GenerateToken(length) {
if (!length || length < 0) {
length = 20
}
const bytes = await util.promisify(crypto.randomBytes)(length)
return bytes.toString('base64')
}
module.exports = {
GenerateToken
}
================================================
FILE: cli/lib/Utils.js
================================================
const fs = require('fs')
const util = require('util')
const path = require('path')
const readdirPromise = util.promisify(fs.readdir)
const unlinkPromise = util.promisify(fs.unlink)
const rmdirPromise = util.promisify(fs.rmdir)
/**
* Deletes all files in a directory, without removing the directory itself.
*
* @param {string} directory - Directory to clean
* @async
*/
async function CleanDirectory(directory) {
const files = await readdirPromise(directory)
return Promise.all(files.map(
(file) => {
const target = path.join(directory, file)
const stat = fs.lstatSync(target)
if (stat.isDirectory()) {
return CleanDirectory(target)
.then(() => rmdirPromise(target))
}
return unlinkPromise(target)
}
))
}
module.exports = {
CleanDirectory
}
================================================
FILE: cli/lib/aes-kw.js
================================================
/**
* This module is based on https://github.com/calvinmetcalf/aes-kw
*
* Copyright (C) Calvin Metcalf. Released under MIT license.
*/
const crypto = require('crypto')
const xor = require('buffer-xor/inplace')
const bufferEq = require('buffer-equal-constant-time')
const IV = Buffer.from('A6A6A6A6A6A6A6A6', 'hex')
const EMPTY_BUF = Buffer.alloc(0)
function Encrypter(key, decipher) {
if (decipher) {
this.cipher = crypto.createDecipheriv(getCipherName(key), key, EMPTY_BUF)
}
else {
this.cipher = crypto.createCipheriv(getCipherName(key), key, EMPTY_BUF)
}
this.cipher.setAutoPadding(false)
}
Encrypter.prototype.encrypt = function(iv, buf) {
if (iv.length !== 8) {
throw new Error('invalid iv length')
}
if (buf.length !== 8) {
throw new Error('invalid data length')
}
this.cipher.update(iv)
return this.cipher.update(buf)
}
Encrypter.prototype.done = function() {
this.cipher.final()
}
function getCipherName(key) {
switch (key.length) {
case 16: return 'aes-128-ecb'
case 24: return 'aes-192-ecb'
case 32: return 'aes-256-ecb'
}
}
function msb(b) {
return b.slice(0, 8)
}
function lsb(b) {
return b.slice(-8)
}
exports.encrypt = encrypt
function encrypt(key, plaintext) {
if (plaintext.length % 8) {
throw new Error('must be 64 bit increment')
}
const enc = new Encrypter(key)
let j = -1
let i, b
const t = Buffer.alloc(8)
let a = IV
const n = plaintext.length / 8
const r = createR(plaintext)
while (++j <= 5) {
i = -1
while (++i < n) {
b = enc.encrypt(a, r[i])
t.writeUInt32BE(0, 0)
t.writeUInt32BE((n * j) + i + 1, 4)
a = xor(msb(b), t)
r[i] = lsb(b)
}
}
enc.done()
return Buffer.concat([a].concat(r))
}
exports.decrypt = decrypt
function decrypt(key, ciphertext) {
if (ciphertext.length % 8) {
throw new Error('must be 64 bit increment')
}
const enc = new Encrypter(key, true)
let j = 6
let i, b
const t = Buffer.alloc(8)
const n = ciphertext.length / 8
const r = createR(ciphertext)
let a = r[0]
while (--j >= 0) {
i = n
while (--i) {
t.writeUInt32BE(0, 0)
t.writeUInt32BE(((n - 1)* j) + i, 4)
a = xor(a, t)
b = enc.encrypt(a, r[i])
a = msb(b)
r[i] = lsb(b)
}
}
enc.done()
if (!bufferEq(a, IV)) {
throw new Error('unable to decrypt')
}
return Buffer.concat(r.slice(1))
}
function createR(buf) {
const n = buf.length / 8
const out = new Array(n)
let i = -1
while (++i < n) {
out[i] = buf.slice(i * 8, (i + 1) * 8)
}
return out
}
================================================
FILE: docs-source/.gitignore
================================================
# Generated files
/content/cli/*.md
!/content/cli/__template.md
/content/menu/*.md
!/content/menu/__template.md
/dist
# Created by https://www.gitignore.io/api/hugo
# Edit at https://www.gitignore.io/?templates=hugo
### Hugo ###
# gitginore template for Hugo projects
# website: https://gohugo.io
# generated files by hugo
/public/
/resources/_gen/
# executable may be added to repository
hugo.exe
hugo.darwin
hugo.linux
# End of https://www.gitignore.io/api/hugo
================================================
FILE: docs-source/config.yaml
================================================
baseURL: "https://hereditas.app/"
languageCode: en-us
title: Hereditas
# Ignore files
ignoreFiles:
- "\\.sh$"
- "Makefile"
- "Dockerfile"
- "__template.md"
# Enable all URLs to be relative, and make them end with ".html"
relativeURLs: true
canonifyURLs: false
uglyurls: true
# Book Theme is intended for documentation use, therefore it doesn't render taxonomy.
# You can hide related warning with config below
disableKinds:
- taxonomy
- taxonomyTerm
- section
# Goldmark
markup:
goldmark:
renderer:
unsafe: true
# Syntax highlighting
pygmentsCodeFences: true
pygmentsStyle: "tango"
# Google analytics
#googleAnalytics: UA-72379106-2
# Privacy
privacy:
googleAnalytics:
anonymizeIP: true
youtube:
privacyEnhanced: true
# Theme
theme: book
# Theme params
params:
# (Optional, default true) Show or hide table of contents globally
# You can also specify this parameter per page in front matter
BookShowToC: true
# (Optional, default none) Set leaf bundle to render as side menu
# When not specified file structure and weights will be used
BookMenuBundle: /menu
# (Optional, default docs) Specify section of content to render as menu
# You can also set value to "*" to render all sections to menu
BookSection: docs
# This value is duplicate of $link-color for making active link highlight in menu bundle mode
# BookMenuBundleActiveLinkColor: \#004ed0
# Include JS scripts in pages. Disabled by default.
# - Keep side menu on same scroll position during navigation
BookEnableJS: false
# Set source repository location.
# Used for 'Last Modified' and 'Edit this page' links.
#BookRepo: https://github.com/ItalyPaleAle/hereditas/docs
# Enable "Edit this page" links for 'doc' page type.
# Disabled by default. Uncomment to enable. Requires 'BookRepo' param.
# Path must point to 'content' directory of repo.
#BookEditPath: edit/master/exampleSite/content
# Plausible Analytics
PlausibleAnalytics:
Domain: hereditas.app
================================================
FILE: docs-source/content/_index.md
================================================
---
title: What is Hereditas
type: docs
---

# What is Hereditas
**What happens to your digital life after you're gone?**
Hereditas, which means *inheritance* in Latin, is a static website generator that builds **fully-trustless digital legacy boxes**, where you can store information for your relatives to access in case of your sudden death or disappearance.
For example, you could use this to pass information such as passwords, cryptographic keys, cryptocurrency wallets, sensitive documents, etc.
{{< youtube lZEKgB5dzQ4 >}}
> Note: the video above was recorded with Hereditas 0.1. The design of the interface has been improved and made nicer in 0.2.
## Why we built Hereditas
Check out the announcement [**blog post**](https://withblue.ink/2019/03/18/what-happens-to-your-digital-life-after-youre-gone-introducing-hereditas.html?utm_source=web&utm_campaign=hereditas-docs) to understand more about why we built Hereditas and why you need it too.
## Design
We've designed Hereditas with three principles in mind.
### Fully trustless – really
With Hereditas, you don't need to trust any person or provider. **No other person or company has standing access to your data.**
As the owner of an Hereditas box, you can nominate some authorized users by whitelisting their email address and giving them a *user passphrase*.
To prevent authorized users from having standing access to your data, however, once they log into your Hereditas box for the first time, they need to wait for a few hours or days before they can unlock the box. This gives you, the owner of the box, enough time to stop the timer, by simply logging into the same Hereditas box.
For example, if you set the waiting time to 24 hours (the default), when a relative of yours tries to log in the timer starts and Hereditas sends you a notification right away. If you've not disappeared, you can log into the same Hereditas box within 24 hours and stop the timer. Without any action from you, after the delay has passed all your relatives would be able to unlock your Hereditas box by logging in again and typing the *user passphrase*.
Hereditas generates digital legacy boxes that are encrypted bundles within static HTML5 applications. The encryption key is split between what you give your users and what's stored inside the authorization provider, so no company or provider has standing access to your data.
### Simple for your loved ones
We designed Hereditas so it's simple to use for your loved ones, when they need to access your digital legacy box, even if they are not tech wizards. **A web browser is all they need.**
As the owner of the Hereditas box, you will provide them with the URL where they can find your box, and the *user passphrase* they need to use to unlock it. You will also whitelist their email address so they can log in with their existing accounts (e.g. Google, Facebook, Microsoft…) – no need to create new accounts for them and have new passwords around.
### No costly and/or time-consuming maintenance
You don't want to rely on a solution that you'll have to keep paying and/or patching for the rest of your life (and in this case, we mean that literally).
**Hereditas outputs a static HTML5 app that you can host anywhere you'd like**, for free or almost.
## Open source
We made Hereditas fully open source so you can study how the app works down to every detail. We wrote the app in JavaScript, and we use Node.js for the CLI and HTML5 for the static web app. **The source code is available on GitHub at [ItalyPaleAle/hereditas](https://github.com/ItalyPaleAle/hereditas)** under GNU General Public License (GPL) version 3.0 (see [LICENSE](https://github.com/ItalyPaleAle/hereditas/tree/master/LICENSE.md)).
We happily accept contributions! Feel free to submit a Pull Request to fix bugs or add new features. Equally important, you can contribute by improving this [documentation](https://github.com/ItalyPaleAle/hereditas/tree/master/docs-source) you're reading.
If you believe you've found a security issue that could impact other people, please [report it confidentially](https://www.npmjs.com/advisories/report?package=hereditas).
## Get started
Ready? Get started with Hereditas now!
<a class="hereditas-button" href="{{< relref "/introduction/quickstart-video.md" >}}">Quickstart Video</a>
Or:
<a class="hereditas-button" href="{{< relref "/guides/get-started.md" >}}">Get started documentation</a>
================================================
FILE: docs-source/content/advanced/auth0-manual-configuration.md
================================================
---
title: Auth0 manual configuration
type: docs
---
# Auth0 manual configuration
Hereditas uses Auth0 to authenticate users and to provide the *application token*, which is part of the string used to derive the encryption key.
This document explains the configuration that the Hereditas CLI performs when you execute the [`hereditas auth0:sync`]({{< relref "/cli/auth0_sync.md" >}}) command.
> **Important:** this page is primarily primarily meant as reference. We recommend letting the Hereditas CLI manage the Auth0 configuration with the [`hereditas auth0:sync`]({{< relref "/cli/auth0_sync.md" >}}) command rather than changing settings manually.
## Diffe
gitextract_d_jnmmz2/ ├── .eslintignore ├── .eslintrc.js ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ ├── docs-ci.yaml │ ├── docs-production.yaml │ └── docs-staging.yaml ├── .gitignore ├── .npmignore ├── .vscode/ │ └── settings.json ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── app/ │ ├── components/ │ │ ├── NavBar.svelte │ │ ├── PassphraseBox.svelte │ │ ├── RequestAuthentication.svelte │ │ └── UserProfile.svelte │ ├── layout/ │ │ └── App.svelte │ ├── lib/ │ │ ├── Base64Utils.js │ │ ├── Box.js │ │ ├── Credentials.js │ │ ├── CryptoUtils.js │ │ ├── StorageService.js │ │ └── Utils.js │ ├── main.css │ ├── main.html │ ├── main.js │ ├── postcss.config.js │ ├── robots.txt │ ├── routes.js │ ├── stores.js │ ├── tailwind.config.js │ ├── views/ │ │ ├── ContentView.svelte │ │ ├── ListView.svelte │ │ └── UnlockView.svelte │ └── webpack.config.js ├── auth0/ │ ├── 01-whitelist.js │ ├── 02-notify.js │ └── 03-wait-logic.js ├── bin/ │ ├── run │ └── run.cmd ├── cli/ │ ├── commands/ │ │ ├── auth0/ │ │ │ └── sync.js │ │ ├── build.js │ │ ├── init.js │ │ ├── pack.js │ │ ├── regenerate-token.js │ │ ├── url/ │ │ │ ├── add.js │ │ │ ├── list.js │ │ │ └── rm.js │ │ ├── user/ │ │ │ ├── add.js │ │ │ ├── list.js │ │ │ └── rm.js │ │ ├── wait-time/ │ │ │ ├── get.js │ │ │ └── set.js │ │ └── webhook/ │ │ ├── get.js │ │ └── set.js │ ├── index.js │ └── lib/ │ ├── Auth0Management.js │ ├── Builder.js │ ├── Config.js │ ├── Content.js │ ├── Crypto.js │ ├── Utils.js │ └── aes-kw.js ├── docs-source/ │ ├── .gitignore │ ├── config.yaml │ ├── content/ │ │ ├── _index.md │ │ ├── advanced/ │ │ │ ├── auth0-manual-configuration.md │ │ │ ├── building-self-contained-binaries.md │ │ │ ├── configuration-file.md │ │ │ └── index-file.md │ │ ├── cli/ │ │ │ └── __template.md │ │ ├── guides/ │ │ │ ├── auth0-setup.md │ │ │ ├── build-static-web-app.md │ │ │ ├── create-box.md │ │ │ ├── deploy-box.md │ │ │ ├── get-started.md │ │ │ ├── login-notifications.md │ │ │ └── managing-users.md │ │ ├── introduction/ │ │ │ ├── quickstart-video.md │ │ │ └── security-model.md │ │ └── menu/ │ │ └── __template.md │ ├── generate-cli-docs.js │ ├── sync-assets.sh │ ├── themes/ │ │ └── book/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── archetypes/ │ │ │ └── docs.md │ │ ├── assets/ │ │ │ ├── _markdown.scss │ │ │ ├── _utils.scss │ │ │ ├── _variables.scss │ │ │ └── book.scss │ │ ├── layouts/ │ │ │ ├── 404.html │ │ │ ├── docs/ │ │ │ │ ├── baseof.html │ │ │ │ ├── list.html │ │ │ │ └── single.html │ │ │ ├── partials/ │ │ │ │ └── docs/ │ │ │ │ ├── brand.html │ │ │ │ ├── git-footer.html │ │ │ │ ├── html-head.html │ │ │ │ ├── inject/ │ │ │ │ │ ├── body.html │ │ │ │ │ ├── head.html │ │ │ │ │ ├── menu-after.html │ │ │ │ │ └── menu-before.html │ │ │ │ ├── menu-bundle.html │ │ │ │ ├── menu-filetree.html │ │ │ │ ├── menu.html │ │ │ │ ├── mobile-header.html │ │ │ │ ├── shared.html │ │ │ │ └── toc.html │ │ │ └── posts/ │ │ │ ├── baseof.html │ │ │ ├── list.html │ │ │ └── single.html │ │ ├── source │ │ └── theme.toml │ ├── workers-site/ │ │ ├── .cargo-ok │ │ ├── .gitignore │ │ ├── assets.js │ │ ├── cache-config.js │ │ ├── index.js │ │ └── package.json │ └── wrangler.toml └── package.json
SYMBOL INDEX (136 symbols across 32 files)
FILE: app/lib/Base64Utils.js
function DecodeArrayBuffer (line 41) | function DecodeArrayBuffer(input) {
function RemovePaddingChars (line 51) | function RemovePaddingChars(input) {
function Decode (line 60) | function Decode(input, arrayBuffer) {
FILE: app/lib/Box.js
class Box (line 7) | class Box {
method constructor (line 8) | constructor() {
method isUnlocked (line 20) | isUnlocked() {
method lock (line 27) | lock() {
method getContents (line 36) | getContents() {
method fetchContent (line 49) | fetchContent(info) {
method fetchIndex (line 104) | fetchIndex() {
method unlock (line 145) | unlock(passphrase, appToken) {
FILE: app/lib/Credentials.js
class Nonce (line 8) | class Nonce {
method constructor (line 9) | constructor() {
method generate (line 19) | generate() {
method retrieve (line 34) | retrieve() {
class Credentials (line 49) | class Credentials {
method constructor (line 50) | constructor() {
method authorizationUrl (line 62) | authorizationUrl() {
method getProfile (line 80) | async getProfile() {
method getToken (line 113) | getToken() {
method setToken (line 142) | async setToken(jwt) {
method _validateToken (line 166) | async _validateToken(jwt) {
FILE: app/lib/CryptoUtils.js
function str2buf (line 8) | function str2buf(str) {
function buf2str (line 17) | function buf2str(buffer) {
function concatBuffers (line 28) | function concatBuffers(buffer1, buffer2) {
function DeriveKeyArgon2 (line 43) | function DeriveKeyArgon2(passphrase, salt) {
function DeriveKeyPBKDF2 (line 75) | function DeriveKeyPBKDF2(passphrase, salt) {
function Decrypt (line 108) | function Decrypt(key, iv, data, tag) {
function UnwrapKey (line 125) | function UnwrapKey(wrappingKey, ciphertext) {
FILE: app/lib/StorageService.js
class StorageService (line 8) | class StorageService {
method constructor (line 12) | constructor() {
method _isSupported (line 28) | _isSupported(type) {
class MemoryStore (line 45) | class MemoryStore {
method constructor (line 49) | constructor() {
method getItem (line 53) | getItem(name) {
method setItem (line 57) | setItem(name, value) {
method removeItem (line 61) | removeItem(name) {
class CookieStore (line 69) | class CookieStore {
method constructor (line 75) | constructor(isSessionStorage) {
method getItem (line 83) | getItem(name) {
method setItem (line 87) | setItem(name, value) {
method removeItem (line 95) | removeItem(name) {
method _updateObject (line 103) | _updateObject() {
FILE: app/lib/Utils.js
function RandomString (line 7) | function RandomString(length = 7) {
function WaitPromise (line 21) | function WaitPromise(time) {
FILE: app/main.js
function getHash (line 13) | function getHash() {
function checkAuthError (line 39) | function checkAuthError(hash) {
function handleSession (line 54) | async function handleSession(hash) {
FILE: app/webpack.config.js
function webpackConfig (line 42) | function webpackConfig(appParams) {
FILE: cli/commands/auth0/sync.js
class Auth0SetupCommand (line 7) | class Auth0SetupCommand extends Command {
method run (line 8) | async run() {
FILE: cli/commands/build.js
class BuildCommand (line 8) | class BuildCommand extends Command {
method run (line 9) | async run() {
FILE: cli/commands/init.js
class InitCommand (line 11) | class InitCommand extends Command {
method run (line 12) | async run() {
FILE: cli/commands/pack.js
class PackCommand (line 12) | class PackCommand extends Command {
method run (line 13) | async run() {
FILE: cli/commands/regenerate-token.js
class RegenerateTokenCommand (line 7) | class RegenerateTokenCommand extends Command {
method run (line 8) | async run() {
FILE: cli/commands/url/add.js
class UrlAddCommand (line 6) | class UrlAddCommand extends Command {
method run (line 7) | async run() {
FILE: cli/commands/url/list.js
class UrlListCommand (line 6) | class UrlListCommand extends Command {
method run (line 7) | async run() {
FILE: cli/commands/url/rm.js
class UrlRmCommand (line 6) | class UrlRmCommand extends Command {
method run (line 7) | async run() {
FILE: cli/commands/user/add.js
class UserAddCommand (line 6) | class UserAddCommand extends Command {
method run (line 7) | async run() {
FILE: cli/commands/user/list.js
class UserListCommand (line 6) | class UserListCommand extends Command {
method run (line 7) | async run() {
FILE: cli/commands/user/rm.js
class UserRmCommand (line 6) | class UserRmCommand extends Command {
method run (line 7) | async run() {
FILE: cli/commands/wait-time/get.js
class WaitTimeGetCommand (line 6) | class WaitTimeGetCommand extends Command {
method run (line 7) | async run() {
FILE: cli/commands/wait-time/set.js
class WaitTimeSetCommand (line 6) | class WaitTimeSetCommand extends Command {
method run (line 7) | async run() {
FILE: cli/commands/webhook/get.js
class WebhookGetCommand (line 6) | class WebhookGetCommand extends Command {
method run (line 7) | async run() {
FILE: cli/commands/webhook/set.js
class WebhookSetCommand (line 6) | class WebhookSetCommand extends Command {
method run (line 7) | async run() {
FILE: cli/lib/Auth0Management.js
class Auth0Management (line 12) | class Auth0Management {
method constructor (line 17) | constructor(config) {
method syncClient (line 38) | async syncClient(clientId) {
method updateClient (line 102) | async updateClient(clientId) {
method createClient (line 117) | async createClient() {
method getClient (line 130) | async getClient(clientId) {
method syncRules (line 144) | async syncRules(ruleIds) {
method listRules (line 177) | listRules() {
method createRules (line 187) | async createRules() {
method listRulesConfigs (line 236) | listRulesConfigs() {
method updateRulesConfigs (line 245) | async updateRulesConfigs() {
method _clientConfiguration (line 268) | _clientConfiguration() {
FILE: cli/lib/Builder.js
class Builder (line 40) | class Builder {
method constructor (line 46) | constructor(passphrase, config) {
method build (line 63) | async build() {
method _deriveKey (line 130) | _deriveKey(passphrase, salt) {
method _createIndex (line 170) | async _createIndex(masterKey, content) {
method _encryptContent (line 192) | async _encryptContent(masterKey, content) {
method _encryptStream (line 230) | async _encryptStream(masterKey, inStream, outStream) {
method _scanContent (line 266) | async _scanContent() {
function includePath (line 312) | function includePath(str) {
FILE: cli/lib/Config.js
class Config (line 46) | class Config {
method constructor (line 52) | constructor(filename) {
method create (line 71) | create(initConfig) {
method load (line 88) | async load() {
method get (line 111) | get(key) {
method all (line 131) | all() {
method set (line 143) | set(key, value) {
method save (line 161) | save() {
method _validate (line 171) | _validate() {
method _defaults (line 203) | _defaults() {
FILE: cli/lib/Content.js
class Content (line 20) | class Content {
method constructor (line 27) | constructor(el, config) {
method el (line 38) | get el() {
method inStream (line 47) | get inStream() {
method process (line 54) | async process() {
method _processBinary (line 69) | async _processBinary() {
method _processText (line 85) | async _processText() {
method _processMarkdown (line 94) | async _processMarkdown() {
FILE: cli/lib/Crypto.js
function GenerateToken (line 12) | async function GenerateToken(length) {
FILE: cli/lib/Utils.js
function CleanDirectory (line 15) | async function CleanDirectory(directory) {
FILE: cli/lib/aes-kw.js
constant EMPTY_BUF (line 12) | const EMPTY_BUF = Buffer.alloc(0)
function Encrypter (line 13) | function Encrypter(key, decipher) {
function getCipherName (line 35) | function getCipherName(key) {
function msb (line 42) | function msb(b) {
function lsb (line 45) | function lsb(b) {
function encrypt (line 49) | function encrypt(key, plaintext) {
function decrypt (line 74) | function decrypt(key, ciphertext) {
function createR (line 102) | function createR(buf) {
FILE: docs-source/workers-site/cache-config.js
function cacheSettings (line 33) | function cacheSettings(urlStr) {
FILE: docs-source/workers-site/index.js
constant DEBUG (line 12) | const DEBUG = false
function handleEvent (line 35) | async function handleEvent(event) {
function requestFromKV (line 119) | async function requestFromKV(event) {
function requestAsset (line 176) | async function requestAsset(useAsset, modifyBody) {
function setCacheHeader (line 226) | function setCacheHeader(statusCode, headers, cacheOpts) {
function setSecurityHeaders (line 242) | function setSecurityHeaders(headers) {
function isAsset (line 263) | function isAsset(url) {
Condensed preview — 120 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (305K chars).
[
{
"path": ".eslintignore",
"chars": 96,
"preview": "# Docs\ndocs-source\n\n# Auth0 rules (they follow a different style)\nauth0\n\n# Test data\ntestfolder\n"
},
{
"path": ".eslintrc.js",
"chars": 3828,
"preview": "module.exports = {\n env: {\n es6: true,\n node: true,\n browser: true\n },\n extends: 'eslint:r"
},
{
"path": ".github/FUNDING.yml",
"chars": 537,
"preview": "github: ItalyPaleAle\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Col"
},
{
"path": ".github/workflows/docs-ci.yaml",
"chars": 3212,
"preview": "# Docs: CI: deploys to dev\n\n# Required secrets:\n# CF_ACCOUNT_ID: Account ID for Cloudflare Workers\n# CF_API_TOKEN: API t"
},
{
"path": ".github/workflows/docs-production.yaml",
"chars": 3368,
"preview": "# Docs: Deploys to production, triggered manually\n\n# Required secrets:\n# CF_ACCOUNT_ID: Account ID for Cloudflare Worker"
},
{
"path": ".github/workflows/docs-staging.yaml",
"chars": 3362,
"preview": "# Docs: Deploys to staging, triggered manually\n\n# Required secrets:\n# CF_ACCOUNT_ID: Account ID for Cloudflare Workers\n#"
},
{
"path": ".gitignore",
"chars": 2714,
"preview": "# Test data\ntestfolder\n\n# OClif manifest\noclif.manifest.json\n\n\n# Created by https://www.gitignore.io/api/node,macos,linu"
},
{
"path": ".npmignore",
"chars": 73,
"preview": ".DS_Store\nassets/\n.vscode/\ndocs-source/\ntestfolder/\nazure-pipelines.yaml\n"
},
{
"path": ".vscode/settings.json",
"chars": 355,
"preview": "{\n \"files.exclude\": {\n \"**/node_modules\": true,\n \"coverage\": true,\n \"coverage.lcov\": true\n },"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 3328,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
},
{
"path": "LICENSE.md",
"chars": 35644,
"preview": "# License\n\nCopyright © 2019-2020 Alessandro Segala @ItalyPaleAle.\n\nThis program is free software: you can redistribute i"
},
{
"path": "README.md",
"chars": 3069,
"preview": "# Hereditas\n\n[](https://open.vscode.dev/"
},
{
"path": "app/components/NavBar.svelte",
"chars": 424,
"preview": "<nav class=\"bg-white fixed w-full z-10 top-0 shadow\">\n <div class=\"w-full lg:w-3/5 container px-2 flex flex-wrap items-"
},
{
"path": "app/components/PassphraseBox.svelte",
"chars": 1991,
"preview": "{#await $box.fetchIndex()}\n <p>Fetching index, please wait…</p>\n{:then response}\n <form class=\"w-full max-w-md\" on"
},
{
"path": "app/components/RequestAuthentication.svelte",
"chars": 757,
"preview": "{#if $authError}\n <h1>Authentication error</h1>\n <p class=\"mb-2\"><b>Error description:</b> {$authError}</p>\n <a"
},
{
"path": "app/components/UserProfile.svelte",
"chars": 1289,
"preview": "<h1>Hello, {$profile.name}!</h1>\n{#if $hereditasProfile.role == 'owner'}\n <p class=\"mb-2\">You're the owner of this He"
},
{
"path": "app/layout/App.svelte",
"chars": 412,
"preview": "<Navbar />\n<div class=\"container w-full lg:w-3/5 px-2 pt-10 lg:pt-10 mt-10\">\n <Router {routes}/>\n <footer class=\"t"
},
{
"path": "app/lib/Base64Utils.js",
"chars": 3360,
"preview": "// Based on: https://github.com/danguer/blog-examples/blob/master/js/base64-binary.js\n\n/*\nCopyright (c) 2011, Daniel Gue"
},
{
"path": "app/lib/Box.js",
"chars": 6751,
"preview": "import {Decrypt, buf2str, UnwrapKey, DeriveKeyArgon2, DeriveKeyPBKDF2} from './CryptoUtils'\nimport {DecodeArrayBuffer} f"
},
{
"path": "app/lib/Credentials.js",
"chars": 5629,
"preview": "import {RandomString} from './Utils'\nimport storage from './StorageService'\nimport IdTokenVerifier from 'idtoken-verifie"
},
{
"path": "app/lib/CryptoUtils.js",
"chars": 3966,
"preview": "// Inspired by https://gist.github.com/tscholl2/dc7dc15dc132ea70a98e8542fefffa28#file-aes-js\n\n/**\n * Encodes a utf8 stri"
},
{
"path": "app/lib/StorageService.js",
"chars": 3142,
"preview": "// This module is based on https://github.com/Acanguven/StorageService/blob/master/storage.js\n// License: MIT https://gi"
},
{
"path": "app/lib/Utils.js",
"chars": 717,
"preview": "/**\n * Returns a random string, useful for example as nonce.\n *\n * @param {number} length - Length of the string\n * @ret"
},
{
"path": "app/main.css",
"chars": 806,
"preview": "/* Tailwind */\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n/* Default styles */\nh1 {\n @apply text-2xl"
},
{
"path": "app/main.html",
"chars": 353,
"preview": "<!doctype html>\n<html lang=\"en\">\n\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, i"
},
{
"path": "app/main.js",
"chars": 3900,
"preview": "// Style\nimport './main.css'\n\n// JavaScript modules\nimport App from './layout/App.svelte'\nimport credentials from './lib"
},
{
"path": "app/postcss.config.js",
"chars": 1045,
"preview": "const path = require('path')\n\nconst production = !process.env.ROLLUP_WATCH\n\nmodule.exports = {\n plugins: [\n re"
},
{
"path": "app/robots.txt",
"chars": 26,
"preview": "User-agent: *\nDisallow: /\n"
},
{
"path": "app/routes.js",
"chars": 315,
"preview": "// Import routes\nimport UnlockView from './views/UnlockView.svelte'\nimport ListView from './views/ListView.svelte'\nimpor"
},
{
"path": "app/stores.js",
"chars": 246,
"preview": "import {writable} from 'svelte/store'\n\nexport const pageTitle = writable('Hereditas')\nexport const profile = writable(nu"
},
{
"path": "app/tailwind.config.js",
"chars": 240,
"preview": "module.exports = {\n theme: {\n extend: {\n\n },\n container: {\n center: true,\n }\n "
},
{
"path": "app/views/ContentView.svelte",
"chars": 3115,
"preview": "{#await contentPromise}\n Loading...\n{:then content}\n <nav aria-label=\"breadcrumb\" class=\"mb-4\">\n <ol class="
},
{
"path": "app/views/ListView.svelte",
"chars": 3571,
"preview": "<nav aria-label=\"breadcrumb\" class=\"mb-4\">\n <ol class=\"list-none p-0 inline-flex\">\n {#if list.paths && list.pa"
},
{
"path": "app/views/UnlockView.svelte",
"chars": 693,
"preview": "{#if $profile}\n <UserProfile />\n{:else}\n <RequestAuthentication />\n{/if}\n\n<h1 class=\"my-4\">About this page</h1>\n<s"
},
{
"path": "app/webpack.config.js",
"chars": 5006,
"preview": "const MiniCssExtractPlugin = require('mini-css-extract-plugin')\nconst HtmlWebpackPlugin = require('html-webpack-plugin')"
},
{
"path": "auth0/01-whitelist.js",
"chars": 797,
"preview": "function (user, context, callback) {\n // Apply this rule only for Hereditas, and bypass it for other apps\n context"
},
{
"path": "auth0/02-notify.js",
"chars": 1519,
"preview": "function (user, context, callback) {\n // Apply this rule only for Hereditas, and bypass it for other apps\n context"
},
{
"path": "auth0/03-wait-logic.js",
"chars": 3707,
"preview": "function (user, context, callback) {\n // Apply this rule only for Hereditas, and bypass it for other apps\n context"
},
{
"path": "bin/run",
"chars": 145,
"preview": "#!/usr/bin/env node\n\nrequire('@oclif/command')\n .run()\n .then(require('@oclif/command/flush'))\n .catch(require("
},
{
"path": "bin/run.cmd",
"chars": 31,
"preview": "@echo off\n\nnode \"%~dp0\\run\" %*\n"
},
{
"path": "cli/commands/auth0/sync.js",
"chars": 1487,
"preview": "'use strict'\n\nconst {Command} = require('@oclif/command')\nconst Config = require('../../lib/Config')\nconst Auth0Manageme"
},
{
"path": "cli/commands/build.js",
"chars": 2217,
"preview": "'use strict'\n\nconst {Command} = require('@oclif/command')\nconst Config = require('../lib/Config')\nconst Builder = requir"
},
{
"path": "cli/commands/init.js",
"chars": 4492,
"preview": "'use strict'\n\nconst {Command, flags} = require('@oclif/command')\nconst fs = require('fs')\nconst util = require('util')\nc"
},
{
"path": "cli/commands/pack.js",
"chars": 6017,
"preview": "'use strict'\n\nconst {Command} = require('@oclif/command')\nconst Config = require('../lib/Config')\nconst util = require('"
},
{
"path": "cli/commands/regenerate-token.js",
"chars": 1557,
"preview": "'use strict'\n\nconst {Command} = require('@oclif/command')\nconst Config = require('../lib/Config')\nconst {GenerateToken} "
},
{
"path": "cli/commands/url/add.js",
"chars": 2286,
"preview": "'use strict'\n\nconst {Command, flags} = require('@oclif/command')\nconst Config = require('../../lib/Config')\n\nclass UrlAd"
},
{
"path": "cli/commands/url/list.js",
"chars": 899,
"preview": "'use strict'\n\nconst {Command} = require('@oclif/command')\nconst Config = require('../../lib/Config')\n\nclass UrlListComma"
},
{
"path": "cli/commands/url/rm.js",
"chars": 1944,
"preview": "'use strict'\n\nconst {Command, flags} = require('@oclif/command')\nconst Config = require('../../lib/Config')\n\nclass UrlRm"
},
{
"path": "cli/commands/user/add.js",
"chars": 2776,
"preview": "'use strict'\n\nconst {Command, flags} = require('@oclif/command')\nconst Config = require('../../lib/Config')\n\nclass UserA"
},
{
"path": "cli/commands/user/list.js",
"chars": 1748,
"preview": "'use strict'\n\nconst {Command, flags} = require('@oclif/command')\nconst Config = require('../../lib/Config')\n\nclass UserL"
},
{
"path": "cli/commands/user/rm.js",
"chars": 1802,
"preview": "'use strict'\n\nconst {Command, flags} = require('@oclif/command')\nconst Config = require('../../lib/Config')\n\nclass UserR"
},
{
"path": "cli/commands/wait-time/get.js",
"chars": 1145,
"preview": "'use strict'\n\nconst {Command} = require('@oclif/command')\nconst Config = require('../../lib/Config')\n\nclass WaitTimeGetC"
},
{
"path": "cli/commands/wait-time/set.js",
"chars": 2121,
"preview": "'use strict'\n\nconst {Command, flags} = require('@oclif/command')\nconst Config = require('../../lib/Config')\n\nclass WaitT"
},
{
"path": "cli/commands/webhook/get.js",
"chars": 1129,
"preview": "'use strict'\n\nconst {Command} = require('@oclif/command')\nconst Config = require('../../lib/Config')\n\nclass WebhookGetCo"
},
{
"path": "cli/commands/webhook/set.js",
"chars": 1985,
"preview": "'use strict'\n\nconst {Command, flags} = require('@oclif/command')\nconst Config = require('../../lib/Config')\n\nclass Webho"
},
{
"path": "cli/index.js",
"chars": 43,
"preview": "module.exports = require('@oclif/command')\n"
},
{
"path": "cli/lib/Auth0Management.js",
"chars": 9744,
"preview": "'use strict'\n\nconst fs = require('fs')\nconst util = require('util')\nconst path = require('path')\n\nconst ManagementClient"
},
{
"path": "cli/lib/Builder.js",
"chars": 11688,
"preview": "'use strict'\n\nconst fs = require('fs')\nconst crypto = require('crypto')\nconst {Readable} = require('stream')\nconst util "
},
{
"path": "cli/lib/Config.js",
"chars": 7523,
"preview": "'use strict'\n\nconst fs = require('fs')\nconst util = require('util')\nconst defaultsDeep = require('lodash.defaultsdeep')\n"
},
{
"path": "cli/lib/Content.js",
"chars": 3354,
"preview": "'use strict'\n\nconst fs = require('fs')\nconst {Readable} = require('stream')\nconst util = require('util')\nconst path = re"
},
{
"path": "cli/lib/Crypto.js",
"chars": 574,
"preview": "'use strict'\n\nconst crypto = require('crypto')\nconst util = require('util')\n\n/**\n * Generates a token with `length` rand"
},
{
"path": "cli/lib/Utils.js",
"chars": 876,
"preview": "const fs = require('fs')\nconst util = require('util')\nconst path = require('path')\n\nconst readdirPromise = util.promisif"
},
{
"path": "cli/lib/aes-kw.js",
"chars": 2811,
"preview": "/**\n * This module is based on https://github.com/calvinmetcalf/aes-kw\n *\n * Copyright (C) Calvin Metcalf. Released unde"
},
{
"path": "docs-source/.gitignore",
"chars": 469,
"preview": "# Generated files\n/content/cli/*.md\n!/content/cli/__template.md\n/content/menu/*.md\n!/content/menu/__template.md\n/dist\n\n#"
},
{
"path": "docs-source/config.yaml",
"chars": 2010,
"preview": "baseURL: \"https://hereditas.app/\"\nlanguageCode: en-us\ntitle: Hereditas\n\n# Ignore files\nignoreFiles:\n - \"\\\\.sh$\"\n - \"Ma"
},
{
"path": "docs-source/content/_index.md",
"chars": 4482,
"preview": "---\ntitle: What is Hereditas\ntype: docs\n---\n\n\n\n# What is Hereditas\n\n**What "
},
{
"path": "docs-source/content/advanced/auth0-manual-configuration.md",
"chars": 4585,
"preview": "---\ntitle: Auth0 manual configuration\ntype: docs\n---\n\n# Auth0 manual configuration\n\nHereditas uses Auth0 to authenticate"
},
{
"path": "docs-source/content/advanced/building-self-contained-binaries.md",
"chars": 2733,
"preview": "---\ntitle: Building self-contained binaries\ntype: docs\n---\n\n# Building self-contained binaries\n\nStarting with Hereditas "
},
{
"path": "docs-source/content/advanced/configuration-file.md",
"chars": 7926,
"preview": "---\ntitle: Configuration file\ntype: docs\n---\n\n# Configuration file\n\nEach Hereditas working folder contains a JSON config"
},
{
"path": "docs-source/content/advanced/index-file.md",
"chars": 2431,
"preview": "---\ntitle: Index file\ntype: docs\n---\n\n# Index file\n\nEach Hereditas box contains an encrypted file named `_index`.\n\n## En"
},
{
"path": "docs-source/content/cli/__template.md",
"chars": 423,
"preview": "---\ntitle: {{{commandName}}}\ntype: docs\n---\n\n# hereditas {{{commandName}}}\n\n{{{shortDescription}}}\n\n## Description\n\n{{{l"
},
{
"path": "docs-source/content/guides/auth0-setup.md",
"chars": 5916,
"preview": "---\ntitle: Auth0 setup\ntype: docs\n---\n\n# Auth0 setup\n\n[Auth0](https://auth0.com/) is an authentication provider built to"
},
{
"path": "docs-source/content/guides/build-static-web-app.md",
"chars": 1891,
"preview": "---\ntitle: Build the static web app\ntype: docs\n---\n\n# Build the static web app\n\nIn the previous step we created an Hered"
},
{
"path": "docs-source/content/guides/create-box.md",
"chars": 4139,
"preview": "---\ntitle: Create the box\ntype: docs\n---\n\n# Create the box\n\nAfter gathering all the content you want to encrypt, setting"
},
{
"path": "docs-source/content/guides/deploy-box.md",
"chars": 6774,
"preview": "---\ntitle: Deploy the box\ntype: docs\n---\n\n# Deploy the box\n\nIn this last step, we're finally ready to deploy the static "
},
{
"path": "docs-source/content/guides/get-started.md",
"chars": 3252,
"preview": "---\ntitle: Get started\ntype: docs\n---\n\n# Get started\n\n## Prerequisites\n\nIn order to use Hereditas, you will need [Node.j"
},
{
"path": "docs-source/content/guides/login-notifications.md",
"chars": 2305,
"preview": "---\ntitle: Login notifications\ntype: docs\n---\n\n# Login notifications\n\nAs the owner of an Hereditas box, you'll want to b"
},
{
"path": "docs-source/content/guides/managing-users.md",
"chars": 2598,
"preview": "---\ntitle: Managing users\ntype: docs\n---\n\n# Managing users\n\nYou can use the Hereditas CLI to add or remove authorized us"
},
{
"path": "docs-source/content/introduction/quickstart-video.md",
"chars": 578,
"preview": "---\ntitle: Quickstart video\ntype: docs\n---\n\n# Quickstart video\n\nThis quickstart video shows you how to get an Hereditas "
},
{
"path": "docs-source/content/introduction/security-model.md",
"chars": 7290,
"preview": "---\ntitle: Security model\ntype: docs\n---\n\n# Security model\n\nThis document explains in technical details how Hereditas en"
},
{
"path": "docs-source/content/menu/__template.md",
"chars": 1325,
"preview": "---\nheadless: true\n---\n{{! Set Mustache delimeters to ASP-style tags (this is a Mustache comment) }}\n{{=<% %>=}}\n\n* **In"
},
{
"path": "docs-source/generate-cli-docs.js",
"chars": 4875,
"preview": "'use strict'\n\nconst Mustache = require('mustache')\nconst fs = require('fs')\nconst util = require('util')\n\n// Promisified"
},
{
"path": "docs-source/sync-assets.sh",
"chars": 1068,
"preview": "#!/bin/sh\n\nset -eu\n\n# \"azcopy\" command, defaults to searching for it in the PATH\n: \"${AZCOPYCMD:=$(which azcopy)}\"\necho "
},
{
"path": "docs-source/themes/book/LICENSE",
"chars": 1077,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2018 Alex Shpak\n\nPermission is hereby granted, free of charge, to any person obtain"
},
{
"path": "docs-source/themes/book/README.md",
"chars": 5107,
"preview": "# Hugo Book Theme\n[](https://travis-ci.org/"
},
{
"path": "docs-source/themes/book/archetypes/docs.md",
"chars": 58,
"preview": "---\ntitle: \"{{ .Name | humanize | title }}\"\nweight: 1\n---\n"
},
{
"path": "docs-source/themes/book/assets/_markdown.scss",
"chars": 1273,
"preview": "@import 'variables';\n\n$block-border-radius: 0.15rem;\n\n.markdown {\n line-height: 1.7;\n\n > :first-child {\n margin-top"
},
{
"path": "docs-source/themes/book/assets/_utils.scss",
"chars": 571,
"preview": ".flex {\n display: flex;\n}\n\n.justify-start {\n justify-content: flex-start;\n}\n\n.justify-end {\n justify-content: flex-en"
},
{
"path": "docs-source/themes/book/assets/_variables.scss",
"chars": 898,
"preview": "$padding-1: 1px;\n$padding-4: 0.25rem;\n$padding-8: 0.5rem;\n$padding-16: 1rem;\n\n$font-size-base: 16px;\n$font-size-12: 0.75"
},
{
"path": "docs-source/themes/book/assets/book.scss",
"chars": 3509,
"preview": "@import \"variables\";\n@import \"markdown\";\n@import \"utils\";\n\nhtml {\n font-size: $font-size-base;\n letter-spacing: 0.33px"
},
{
"path": "docs-source/themes/book/layouts/404.html",
"chars": 280,
"preview": "<!DOCTYPE html>\n{{- partial \"docs/shared\" -}}\n<html>\n\n<head>\n {{ partial \"docs/html-head\" . }}\n {{ partial \"docs/injec"
},
{
"path": "docs-source/themes/book/layouts/docs/baseof.html",
"chars": 579,
"preview": "<!DOCTYPE html>\n{{- partial \"docs/shared\" -}}\n<html>\n\n<head>\n {{ partial \"docs/html-head\" . }}\n {{ partial \"docs/injec"
},
{
"path": "docs-source/themes/book/layouts/docs/list.html",
"chars": 146,
"preview": "{{ define \"main\" }}\n<article class=\"markdown\">\n {{- .Content -}}\n</article>\n{{ end }}\n\n{{ define \"toc\" }}\n {{ partial "
},
{
"path": "docs-source/themes/book/layouts/docs/single.html",
"chars": 146,
"preview": "{{ define \"main\" }}\n<article class=\"markdown\">\n {{- .Content -}}\n</article>\n{{ end }}\n\n{{ define \"toc\" }}\n {{ partial "
},
{
"path": "docs-source/themes/book/layouts/partials/docs/brand.html",
"chars": 83,
"preview": "<h2 class=\"book-brand\">\n <a href=\"{{ .Site.BaseURL }}\">{{ .Site.Title }}</a>\n</h2>"
},
{
"path": "docs-source/themes/book/layouts/partials/docs/git-footer.html",
"chars": 793,
"preview": "{{ if or .GitInfo .Site.Params.BookEditPath }}\n<div class=\"align-center book-git-footer {{ if not .GitInfo }}justify-end"
},
{
"path": "docs-source/themes/book/layouts/partials/docs/html-head.html",
"chars": 595,
"preview": "<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>{{- template \"title"
},
{
"path": "docs-source/themes/book/layouts/partials/docs/inject/body.html",
"chars": 220,
"preview": "{{ if eq hugo.Environment \"production\" }}\n {{ with .Site.Params.PlausibleAnalytics }}\n<script async defer data-domain=\""
},
{
"path": "docs-source/themes/book/layouts/partials/docs/inject/head.html",
"chars": 0,
"preview": ""
},
{
"path": "docs-source/themes/book/layouts/partials/docs/inject/menu-after.html",
"chars": 0,
"preview": ""
},
{
"path": "docs-source/themes/book/layouts/partials/docs/inject/menu-before.html",
"chars": 0,
"preview": ""
},
{
"path": "docs-source/themes/book/layouts/partials/docs/menu-bundle.html",
"chars": 186,
"preview": "{{- template \"hrefhack\" . -}}\n{{ with .Site.GetPage .Site.Params.BookMenuBundle }}\n {{- .Content -}}\n{{ end }}\n{{ if .S"
},
{
"path": "docs-source/themes/book/layouts/partials/docs/menu-filetree.html",
"chars": 1477,
"preview": "<!-- Put configured sections list to .Scratch -->\n{{ template \"book-get-root-section\" . }} \n\n{{- range .Scratch.Get \"Boo"
},
{
"path": "docs-source/themes/book/layouts/partials/docs/menu.html",
"chars": 282,
"preview": "<nav role=\"navigation\">\n{{ partial \"docs/brand\" . }}\n{{ partial \"docs/inject/menu-before\" . }}\n\n{{ if .Site.Params.BookM"
},
{
"path": "docs-source/themes/book/layouts/partials/docs/mobile-header.html",
"chars": 201,
"preview": "<header class=\"align-center justify-between book-header\">\n <label for=\"menu-control\">\n <img src=\"{{ \"svg/menu.svg\" |"
},
{
"path": "docs-source/themes/book/layouts/partials/docs/shared.html",
"chars": 1062,
"preview": "{{/*These templates contains some more complex logic and shared between partials*/}}\n{{- define \"title\" -}}\n {{- if .Pa"
},
{
"path": "docs-source/themes/book/layouts/partials/docs/toc.html",
"chars": 222,
"preview": "{{ $showToC := default (default true .Site.Params.BookShowToC) .Params.bookshowtoc }}\n {{ if and ($showToC) (.Page.Tabl"
},
{
"path": "docs-source/themes/book/layouts/posts/baseof.html",
"chars": 520,
"preview": "<!DOCTYPE html>\n{{- partial \"docs/shared\" -}}\n<html>\n\n<head>\n {{ partial \"docs/html-head\" . }}\n {{ partial \"docs/injec"
},
{
"path": "docs-source/themes/book/layouts/posts/list.html",
"chars": 519,
"preview": "{{ define \"main\" }}\n {{ $paginator := .Paginate (where .Pages \"Params.hidden\" \"ne\" true) }}\n {{ range sort .Paginator."
},
{
"path": "docs-source/themes/book/layouts/posts/single.html",
"chars": 203,
"preview": "{{ define \"main\" }}\n<header>\n <h1>{{ .Title }}</h1>\n <h5>\n <strong>{{ .Date.Format \"January 2, 2006\" }}</strong>\n "
},
{
"path": "docs-source/themes/book/source",
"chars": 85,
"preview": "https://github.com/alex-shpak/hugo-book/tree/fdc6fdd2de1bcb5a891fd3e76c7d4cacb596ee09"
},
{
"path": "docs-source/themes/book/theme.toml",
"chars": 514,
"preview": "# theme.toml template for a Hugo theme\n# See https://github.com/gohugoio/hugoThemes#themetoml for an example\n\nname = \"Bo"
},
{
"path": "docs-source/workers-site/.cargo-ok",
"chars": 0,
"preview": ""
},
{
"path": "docs-source/workers-site/.gitignore",
"chars": 20,
"preview": "node_modules\nworker\n"
},
{
"path": "docs-source/workers-site/assets.js",
"chars": 589,
"preview": "export default [\n {\n match: /^\\/images\\/(.*?)$/,\n storagePath: '/public/images/$1',\n // Cache in"
},
{
"path": "docs-source/workers-site/cache-config.js",
"chars": 1599,
"preview": "// Configure how to cache files from KV\n\n// Match by path\nexport const cachePaths = [\n {\n // JS and CSS files\n"
},
{
"path": "docs-source/workers-site/index.js",
"chars": 9673,
"preview": "import {getAssetFromKV} from '@cloudflare/kv-asset-handler'\nimport assets from './assets'\nimport {cacheSettings} from '."
},
{
"path": "docs-source/workers-site/package.json",
"chars": 307,
"preview": "{\n \"private\": true,\n \"name\": \"worker\",\n \"version\": \"1.0.0\",\n \"description\": \"A template for kick starting a Cloudfla"
},
{
"path": "docs-source/wrangler.toml",
"chars": 818,
"preview": "compatibility_date = \"2021-11-08\"\ntype = \"webpack\"\nname = \"hereditas-dev\"\n# Dev environment is deployed to a workers.dev"
},
{
"path": "package.json",
"chars": 2769,
"preview": "{\n \"name\": \"hereditas\",\n \"version\": \"0.2.2\",\n \"author\": \"Alessandro Segala @ItalyPaleAle\",\n \"bin\": {\n \"hereditas\""
}
]
About this extraction
This page contains the full source code of the ItalyPaleAle/hereditas GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 120 files (280.5 KB), approximately 71.6k tokens, and a symbol index with 136 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.