Add the Independent Proof LLC holding page
A one-page "notice of preparation" in a certificate style: the company is in preparation, there is nothing to do yet, the domain is not for sale, and unsolicited inquiries are not accepted. It carries no contact details of any kind, and search engines are told to stay away (noindex meta and robots.txt). Fonts are self-hosted with their OFL licences, and the seal and guilloche border are static SVG, so the page makes no third-party requests and needs no script. The build copies public/ to dist/, and the Start9 Pages deploy uses the same NextExplorer script as the YPBA site. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012NBdmeSPq4bFCSE1urk99W
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
||||
# Deploy targets and credentials: server hostnames, users, API keys
|
||||
*.local.env
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Build output and tooling
|
||||
dist/
|
||||
node_modules/
|
||||
.DS_Store
|
||||
@@ -0,0 +1,69 @@
|
||||
# Independent Proof LLC website
|
||||
|
||||
Source for independentproofllc.com. independentproofllc.org serves the same site.
|
||||
|
||||
Today the site is a single holding page. It says the company is in preparation, that there is
|
||||
nothing to do yet, that the domain is not for sale, and that unsolicited inquiries are not accepted.
|
||||
It deliberately carries no email address, phone number, postal address, form, social links or
|
||||
names, so there is nothing on it for a scraper to harvest. Search engines are told to stay away,
|
||||
both in the page (`noindex`) and in `robots.txt`.
|
||||
|
||||
## Working on it
|
||||
|
||||
```bash
|
||||
npm run build # copies public/ to dist/, which is what every deploy sends
|
||||
```
|
||||
|
||||
There are no dependencies to install. Open `public/index.html` in a browser to preview it.
|
||||
|
||||
| To change | Edit |
|
||||
| --- | --- |
|
||||
| The page's wording, layout or seal | `public/index.html` (the styles are at the top) |
|
||||
| Whether search engines may index it | the `robots` meta tag in `public/index.html`, and `public/robots.txt` |
|
||||
| Fonts | `public/fonts/`: Cinzel, EB Garamond and Pinyon Script, latin subset, self-hosted so a visit makes no third-party request. Each font's licence (SIL OFL 1.1) sits beside it. |
|
||||
|
||||
The seal's scalloped edge, rosette and the page's guilloche border are generated geometry, pasted
|
||||
into the page as static SVG so it needs no script.
|
||||
|
||||
## Deploying
|
||||
|
||||
### Start9 Pages (StartOS test server)
|
||||
|
||||
The same method as the YPBA site: the files live in **NextExplorer**, and Start9 Pages serves them
|
||||
from there. `scripts/nextexplorer.mjs` talks to NextExplorer's API, and its `deploy` command
|
||||
publishes the site.
|
||||
|
||||
1. Copy `deploy.local.env.example` to `deploy.local.env` (git-ignored) and fill it in. The password
|
||||
goes in its own mode-600 file, never in that file.
|
||||
2. Deploy:
|
||||
|
||||
```bash
|
||||
npm run deploy:pages # build and publish
|
||||
npm run deploy:pages -- --dry-run # show what would be sent
|
||||
npm run nx -- rollback # put the previous version back
|
||||
```
|
||||
|
||||
3. After the first deploy only: in StartOS, open Start9 Pages → Actions → **Manage Websites**, and
|
||||
add a site with source NextExplorer and the path that `deploy` prints.
|
||||
|
||||
NextExplorer never overwrites on upload, so `deploy` uploads the build into a fresh folder and then
|
||||
swaps it into place. One previous version is kept as `<name>.previous`.
|
||||
|
||||
### Public host
|
||||
|
||||
Not chosen yet. See `TODO.md`.
|
||||
|
||||
## Layout
|
||||
|
||||
| Path | What |
|
||||
| --- | --- |
|
||||
| `public/` | The site, served as it is |
|
||||
| `scripts/build.mjs` | Builds `dist/` |
|
||||
| `scripts/nextexplorer.mjs` | NextExplorer file access and the Start9 Pages deploy (shared with the YPBA site's repo) |
|
||||
| `TODO.md` | What's left |
|
||||
|
||||
## Rules for this repo
|
||||
|
||||
- Server hostnames, LAN addresses, usernames and passwords go only in git-ignored files
|
||||
(`deploy.local.env`). The public domains are the site's content and belong here.
|
||||
- Keep the page free of contact details of any kind until that is a deliberate decision.
|
||||
@@ -0,0 +1,21 @@
|
||||
# TODO
|
||||
|
||||
## Before the public launch of the holding page
|
||||
|
||||
- [ ] Choose the cloud host. Deploy to it the same way for this site and the YPBA site: each repo
|
||||
builds `dist/`, and one deploy command per host sends it (for example rsync or SFTP over SSH,
|
||||
with the host and user in the git-ignored `deploy.local.env`).
|
||||
- [ ] Point independentproofllc.com (apex and `www`) at the host, with HTTPS.
|
||||
- [ ] Make independentproofllc.org and `www.independentproofllc.org` answer with a permanent (301)
|
||||
redirect to https://independentproofllc.com, so the site has one address.
|
||||
- [ ] Before either domain gets mail records, decide on email. Avoid a catch-all mailbox: spammers
|
||||
guess addresses like `info@`.
|
||||
|
||||
## Later
|
||||
|
||||
- [ ] Brand: logo, colours and typefaces. The seal on the holding page is a placeholder.
|
||||
- [ ] Favicon (the page currently tells browsers there is none).
|
||||
- [ ] Build out the real site in this repo. Its output still goes to `dist/`, so the deploy
|
||||
commands keep working.
|
||||
- [ ] Decide when search engines may index the site; then remove the `robots` meta tag and change
|
||||
`public/robots.txt`.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copy to deploy.local.env (git-ignored) and fill in. Used by scripts/nextexplorer.mjs.
|
||||
NX_URL=https://<server>.local:<nextexplorer-port>
|
||||
NX_USER=<account email>
|
||||
# The password goes in its own file, mode 600, never here:
|
||||
# ~/.config/nextexplorer/<account name>.password (or point NX_PASS_FILE elsewhere)
|
||||
# The StartOS server's root CA, so TLS is verified:
|
||||
NX_CA_FILE=~/.config/nextexplorer/<server>-root-ca.crt
|
||||
# Folder to publish to: <Location>/independentproof, or share:<share label>/independentproof
|
||||
NX_DEST=share:<label>/independentproof
|
||||
# That folder as Start9 Pages names it, from NextExplorer's storage root (needed for a share)
|
||||
NX_PAGES_PATH=Files/<folder>/independentproof
|
||||
# The address Start9 Pages gives the site, printed after a deploy
|
||||
SITE_URL=
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "independentproof-website",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "node scripts/build.mjs",
|
||||
"deploy:pages": "node scripts/nextexplorer.mjs deploy",
|
||||
"nx": "node scripts/nextexplorer.mjs"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The Cinzel Project Authors (https://github.com/NDISCOVER/Cinzel)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2017 The EB Garamond Project Authors (https://github.com/octaviopardo/EBGaramond12)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2024 The Pinyon Project Authors (https://github.com/SorkinType/Pinyon)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,164 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!-- Hidden from search engines for now. robots.txt says the same. -->
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<title>Independent Proof LLC</title>
|
||||
<!-- No favicon yet; this stops browsers asking for one. -->
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="preload" href="fonts/cinzel-latin.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="fonts/eb-garamond-latin.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<style>
|
||||
/* Self-hosted so a visit makes no request to a third party. Licences are beside the files. */
|
||||
@font-face { font-family: 'Cinzel'; font-style: normal; font-weight: 400 600; font-display: swap; src: url(fonts/cinzel-latin.woff2) format('woff2'); }
|
||||
@font-face { font-family: 'EB Garamond'; font-style: normal; font-weight: 400 500; font-display: swap; src: url(fonts/eb-garamond-latin.woff2) format('woff2'); }
|
||||
@font-face { font-family: 'EB Garamond'; font-style: italic; font-weight: 400; font-display: swap; src: url(fonts/eb-garamond-italic-latin.woff2) format('woff2'); }
|
||||
@font-face { font-family: 'Pinyon Script'; font-style: normal; font-weight: 400; font-display: swap; src: url(fonts/pinyon-script-latin.woff2) format('woff2'); }
|
||||
|
||||
:root {
|
||||
--desk: #2c2a25;
|
||||
--paper: #f2e8cf;
|
||||
--paper-2: #e8dab8;
|
||||
--ink: #2a2118;
|
||||
--ink-2: #5b4a35;
|
||||
--line: #6f5a3a;
|
||||
--green: #3f5b4a;
|
||||
--footer: #b9ae96;
|
||||
--guilloche: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='48' height='16'><path d='M0 8L1 8.85L2 9.68L3 10.49L4 11.25L5 11.96L6 12.6L7 13.16L8 13.63L9 14.01L10 14.28L11 14.44L12 14.5L13 14.44L14 14.28L15 14.01L16 13.63L17 13.16L18 12.6L19 11.96L20 11.25L21 10.49L22 9.68L23 8.85L24 8L25 7.15L26 6.32L27 5.51L28 4.75L29 4.04L30 3.4L31 2.84L32 2.37L33 1.99L34 1.72L35 1.56L36 1.5L37 1.56L38 1.72L39 1.99L40 2.37L41 2.84L42 3.4L43 4.04L44 4.75L45 5.51L46 6.32L47 7.15L48 8' fill='none' stroke='%233f5b4a' stroke-opacity='.55' stroke-width='.5'/><path d='M0 12.6L1 13.16L2 13.63L3 14.01L4 14.28L5 14.44L6 14.5L7 14.44L8 14.28L9 14.01L10 13.63L11 13.16L12 12.6L13 11.96L14 11.25L15 10.49L16 9.68L17 8.85L18 8L19 7.15L20 6.32L21 5.51L22 4.75L23 4.04L24 3.4L25 2.84L26 2.37L27 1.99L28 1.72L29 1.56L30 1.5L31 1.56L32 1.72L33 1.99L34 2.37L35 2.84L36 3.4L37 4.04L38 4.75L39 5.51L40 6.32L41 7.15L42 8L43 8.85L44 9.68L45 10.49L46 11.25L47 11.96L48 12.6' fill='none' stroke='%233f5b4a' stroke-opacity='.55' stroke-width='.5'/><path d='M0 14.5L1 14.44L2 14.28L3 14.01L4 13.63L5 13.16L6 12.6L7 11.96L8 11.25L9 10.49L10 9.68L11 8.85L12 8L13 7.15L14 6.32L15 5.51L16 4.75L17 4.04L18 3.4L19 2.84L20 2.37L21 1.99L22 1.72L23 1.56L24 1.5L25 1.56L26 1.72L27 1.99L28 2.37L29 2.84L30 3.4L31 4.04L32 4.75L33 5.51L34 6.32L35 7.15L36 8L37 8.85L38 9.68L39 10.49L40 11.25L41 11.96L42 12.6L43 13.16L44 13.63L45 14.01L46 14.28L47 14.44L48 14.5' fill='none' stroke='%233f5b4a' stroke-opacity='.55' stroke-width='.5'/><path d='M0 12.6L1 11.96L2 11.25L3 10.49L4 9.68L5 8.85L6 8L7 7.15L8 6.32L9 5.51L10 4.75L11 4.04L12 3.4L13 2.84L14 2.37L15 1.99L16 1.72L17 1.56L18 1.5L19 1.56L20 1.72L21 1.99L22 2.37L23 2.84L24 3.4L25 4.04L26 4.75L27 5.51L28 6.32L29 7.15L30 8L31 8.85L32 9.68L33 10.49L34 11.25L35 11.96L36 12.6L37 13.16L38 13.63L39 14.01L40 14.28L41 14.44L42 14.5L43 14.44L44 14.28L45 14.01L46 13.63L47 13.16L48 12.6' fill='none' stroke='%233f5b4a' stroke-opacity='.55' stroke-width='.5'/></svg>");
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--desk);
|
||||
color: var(--ink);
|
||||
font: 400 18px/1.6 "EB Garamond", Garamond, "Times New Roman", serif;
|
||||
padding: clamp(16px, 4vw, 56px) 16px;
|
||||
}
|
||||
.sheet {
|
||||
max-width: 860px;
|
||||
margin: 0 auto;
|
||||
background:
|
||||
radial-gradient(ellipse at 20% 10%, rgba(255,255,255,.45), transparent 50%),
|
||||
radial-gradient(ellipse at 90% 100%, rgba(120,90,40,.18), transparent 55%),
|
||||
linear-gradient(var(--paper), var(--paper-2));
|
||||
padding: clamp(10px, 1.8vw, 18px);
|
||||
box-shadow: 0 30px 60px rgba(0,0,0,.45), 0 2px 0 rgba(255,255,255,.2) inset;
|
||||
}
|
||||
.band {
|
||||
padding: clamp(10px, 1.6vw, 16px);
|
||||
background: var(--guilloche) repeat, rgba(63, 91, 74, 0.06);
|
||||
border: 1px solid var(--green);
|
||||
}
|
||||
.inner {
|
||||
background: linear-gradient(var(--paper), var(--paper-2));
|
||||
border: 3px double var(--line);
|
||||
padding: clamp(24px, 5vw, 56px) clamp(18px, 5vw, 64px);
|
||||
text-align: center;
|
||||
}
|
||||
.kicker {
|
||||
font: 600 12px/1 Cinzel, "Trajan Pro", serif;
|
||||
letter-spacing: 0.34em;
|
||||
color: var(--green);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
h1 {
|
||||
margin: 14px 0 4px;
|
||||
font: 600 clamp(28px, 5vw, 50px)/1.1 Cinzel, "Trajan Pro", serif;
|
||||
letter-spacing: 0.06em;
|
||||
text-wrap: balance;
|
||||
}
|
||||
.sub { margin: 0; font-style: italic; color: var(--ink-2); }
|
||||
.flourish { display: block; margin: 20px auto; width: min(280px, 70%); height: 14px; color: var(--line); }
|
||||
.script {
|
||||
font: 400 clamp(30px, 5vw, 46px)/1.1 "Pinyon Script", "Snell Roundhand", cursive;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.body { max-width: 56ch; margin: 0 auto; text-wrap: pretty; }
|
||||
.body strong { font-weight: 500; font-variant: small-caps; letter-spacing: .04em; font-size: 1.08em; }
|
||||
ol {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 22px auto 0;
|
||||
max-width: 56ch;
|
||||
text-align: left;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
ol li { display: grid; grid-template-columns: 5.4em 1fr; }
|
||||
ol li b { font-variant: small-caps; font-weight: 500; letter-spacing: .06em; color: var(--green); }
|
||||
.attest {
|
||||
margin-top: 36px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: end;
|
||||
gap: 16px;
|
||||
}
|
||||
.sig { border-top: 1px solid var(--ink-2); padding-top: 6px; font: 400 13px/1.6 Cinzel, serif; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-2); }
|
||||
.seal { width: clamp(120px, 20vw, 160px); height: auto; filter: drop-shadow(0 3px 3px rgba(60,40,10,.4)); }
|
||||
footer { text-align: center; color: var(--footer); font: 400 13px/1.6 Cinzel, serif; letter-spacing: .14em; text-transform: uppercase; margin-top: 22px; }
|
||||
@media (max-width: 560px) {
|
||||
ol li { grid-template-columns: 1fr; }
|
||||
.attest { grid-template-columns: 1fr; justify-items: center; }
|
||||
.attest .sig { width: 80%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="sheet">
|
||||
<div class="band">
|
||||
<div class="inner">
|
||||
<div class="kicker">Notice of Preparation</div>
|
||||
<h1>Independent Proof LLC</h1>
|
||||
<p class="sub">in the year two thousand twenty-six</p>
|
||||
<svg class="flourish" viewBox="0 0 280 14" aria-hidden="true"><path d="M0 7h110M170 7h110" stroke="currentColor" stroke-width="1"/><path d="M118 7c8-8 14-8 22 0s14 8 22 0" fill="none" stroke="currentColor" stroke-width="1.2"/><circle cx="140" cy="7" r="2.4" fill="currentColor"/></svg>
|
||||
<p class="script">Be it known to all who arrive here</p>
|
||||
<p class="body">that <strong>Independent Proof LLC</strong> is presently in preparation, and that this site will open in due course. Be it further noted:</p>
|
||||
<ol>
|
||||
<li><b>First,</b> <span>that there is nothing to be done at this time. There is no list to join and no form to complete.</span></li>
|
||||
<li><b>Second,</b> <span>that this domain is not for sale, and no offer for it will be entertained.</span></li>
|
||||
<li><b>Third,</b> <span>that unsolicited inquiries are not accepted, and for that reason no contact particulars are recorded herein.</span></li>
|
||||
</ol>
|
||||
<div class="attest">
|
||||
<div class="sig">Attested</div>
|
||||
<!-- The seal: the name arcs over the top, the year sits upright along the bottom.
|
||||
Both paths are whole circles so the text can be centred with startOffset 50%:
|
||||
"top" runs clockwise from 6 o'clock (its midpoint is 12), "bottom" runs
|
||||
anticlockwise from 12 o'clock (its midpoint is 6). -->
|
||||
<svg class="seal" viewBox="-80 -80 160 160" role="img" aria-label="Seal of Independent Proof LLC, 2026">
|
||||
<defs>
|
||||
<radialGradient id="foil" cx="35%" cy="30%" r="80%">
|
||||
<stop offset="0" stop-color="#f3dc9a"/>
|
||||
<stop offset=".55" stop-color="#b58a3c"/>
|
||||
<stop offset="1" stop-color="#7a5a20"/>
|
||||
</radialGradient>
|
||||
<path id="seal-top" d="M 0 48 A 48 48 0 1 1 0 -48 A 48 48 0 1 1 0 48"/>
|
||||
<path id="seal-bottom" d="M 0 -56 A 56 56 0 1 0 0 56 A 56 56 0 1 0 0 -56"/>
|
||||
</defs>
|
||||
<polygon fill="url(#foil)" stroke="#7a5a20" stroke-width=".8" points="74,0 71.19,2.49 66.6,4.66 66.4,6.98 70.54,9.91 72.88,12.85 69.68,14.81 64.78,16.15 64.18,18.4 67.75,22.01 69.54,25.31 66.05,26.69 60.99,27.16 60.01,29.27 62.9,33.44 64.09,37 60.41,37.75 55.35,37.33 54.01,39.24 56.13,43.86 56.69,47.57 52.94,47.67 48.03,46.38 46.38,48.03 47.67,52.94 47.57,56.69 43.86,56.13 39.24,54.01 37.33,55.35 37.75,60.41 37,64.09 33.44,62.9 29.27,60.01 27.16,60.99 26.69,66.05 25.31,69.54 22.01,67.75 18.4,64.18 16.15,64.78 14.81,69.68 12.85,72.88 9.91,70.54 6.98,66.4 4.66,66.6 2.49,71.19 0,74 -2.49,71.19 -4.66,66.6 -6.98,66.4 -9.91,70.54 -12.85,72.88 -14.81,69.68 -16.15,64.78 -18.4,64.18 -22.01,67.75 -25.31,69.54 -26.69,66.05 -27.16,60.99 -29.27,60.01 -33.44,62.9 -37,64.09 -37.75,60.41 -37.33,55.35 -39.24,54.01 -43.86,56.13 -47.57,56.69 -47.67,52.94 -46.38,48.03 -48.03,46.38 -52.94,47.67 -56.69,47.57 -56.13,43.86 -54.01,39.24 -55.35,37.33 -60.41,37.75 -64.09,37 -62.9,33.44 -60.01,29.27 -60.99,27.16 -66.05,26.69 -69.54,25.31 -67.75,22.01 -64.18,18.4 -64.78,16.15 -69.68,14.81 -72.88,12.85 -70.54,9.91 -66.4,6.98 -66.6,4.66 -71.19,2.49 -74,0 -71.19,-2.49 -66.6,-4.66 -66.4,-6.98 -70.54,-9.91 -72.88,-12.85 -69.68,-14.81 -64.78,-16.15 -64.18,-18.4 -67.75,-22.01 -69.54,-25.31 -66.05,-26.69 -60.99,-27.16 -60.01,-29.27 -62.9,-33.44 -64.09,-37 -60.41,-37.75 -55.35,-37.33 -54.01,-39.24 -56.13,-43.86 -56.69,-47.57 -52.94,-47.67 -48.03,-46.38 -46.38,-48.03 -47.67,-52.94 -47.57,-56.69 -43.86,-56.13 -39.24,-54.01 -37.33,-55.35 -37.75,-60.41 -37,-64.09 -33.44,-62.9 -29.27,-60.01 -27.16,-60.99 -26.69,-66.05 -25.31,-69.54 -22.01,-67.75 -18.4,-64.18 -16.15,-64.78 -14.81,-69.68 -12.85,-72.88 -9.91,-70.54 -6.98,-66.4 -4.66,-66.6 -2.49,-71.19 0,-74 2.49,-71.19 4.66,-66.6 6.98,-66.4 9.91,-70.54 12.85,-72.88 14.81,-69.68 16.15,-64.78 18.4,-64.18 22.01,-67.75 25.31,-69.54 26.69,-66.05 27.16,-60.99 29.27,-60.01 33.44,-62.9 37,-64.09 37.75,-60.41 37.33,-55.35 39.24,-54.01 43.86,-56.13 47.57,-56.69 47.67,-52.94 46.38,-48.03 48.03,-46.38 52.94,-47.67 56.69,-47.57 56.13,-43.86 54.01,-39.24 55.35,-37.33 60.41,-37.75 64.09,-37 62.9,-33.44 60.01,-29.27 60.99,-27.16 66.05,-26.69 69.54,-25.31 67.75,-22.01 64.18,-18.4 64.78,-16.15 69.68,-14.81 72.88,-12.85 70.54,-9.91 66.4,-6.98 66.6,-4.66 71.19,-2.49"/>
|
||||
<circle r="60" fill="none" stroke="rgba(255,240,200,.7)" stroke-width="1"/>
|
||||
<circle r="44" fill="none" stroke="rgba(90,60,15,.6)" stroke-width="1"/>
|
||||
<g fill="none" stroke="rgba(255,244,210,.75)" stroke-width=".45">
|
||||
<path d="M40 0L39.87 0.7L39.49 1.38L38.86 2.04L38 2.66L36.93 3.23L35.68 3.75L34.28 4.21L32.77 4.61L31.18 4.94L29.54 5.21L27.91 5.43L26.32 5.59L24.81 5.73L23.41 5.84L22.15 5.93L21.06 6.04L20.17 6.17L19.49 6.33L19.03 6.55L18.79 6.84L18.79 7.21L19 7.68L19.41 8.24L20.02 8.91L20.78 9.69L21.68 10.57L22.69 11.56L23.76 12.63L24.87 13.79L25.98 15L27.06 16.26L28.06 17.54L28.97 18.81L29.74 20.06L30.37 21.26L30.82 22.39L31.07 23.42L31.13 24.33L30.99 25.1L30.64 25.71L30.1 26.16L29.36 26.44L28.46 26.54L27.4 26.46L26.21 26.21L24.92 25.81L23.56 25.26L22.14 24.59L20.71 23.82L19.28 22.98L17.9 22.1L16.57 21.21L15.32 20.33L14.18 19.52L13.15 18.78L12.25 18.16L11.49 17.69L10.86 17.38L10.36 17.25L10 17.32L9.76 17.6L9.62 18.09L9.57 18.79L9.6 19.69L9.69 20.78L9.81 22.04L9.95 23.44L10.08 24.95L10.19 26.55L10.26 28.19L10.28 29.84L10.23 31.47L10.1 33.03L9.89 34.49L9.59 35.81L9.21 36.96L8.75 37.91L8.21 38.65L7.61 39.14L6.95 39.39L6.24 39.39L5.5 39.13L4.74 38.62L3.98 37.88L3.23 36.93L2.5 35.79L1.81 34.49L1.15 33.07L0.55 31.56L0 30L-0.5 28.43L-0.94 26.89L-1.33 25.43L-1.68 24.06L-2 22.84L-2.29 21.79L-2.57 20.93L-2.85 20.29L-3.15 19.88L-3.47 19.7L-3.84 19.75L-4.26 20.04L-4.74 20.55L-5.3 21.26L-5.93 22.15L-6.65 23.19L-7.44 24.35L-8.32 25.59L-9.26 26.89L-10.26 28.19L-11.31 29.47L-12.4 30.68L-13.5 31.79L-14.59 32.78L-15.67 33.6L-16.7 34.24L-17.66 34.67L-18.55 34.89L-19.33 34.88L-20 34.64L-20.54 34.18L-20.94 33.51L-21.19 32.63L-21.3 31.58L-21.26 30.37L-21.09 29.03L-20.79 27.58L-20.37 26.08L-19.86 24.53L-19.28 22.98L-18.66 21.46L-18.01 20L-17.36 18.62L-16.76 17.35L-16.21 16.21L-15.76 15.22L-15.42 14.38L-15.23 13.71L-15.19 13.2L-15.32 12.86L-15.64 12.66L-16.15 12.61L-16.84 12.69L-17.73 12.88L-18.78 13.15L-20 13.49L-21.35 13.87L-22.82 14.26L-24.37 14.65L-25.98 15L-27.61 15.3L-29.22 15.53L-30.78 15.68L-32.25 15.73L-33.6 15.67L-34.8 15.49L-35.82 15.2L-36.63 14.8L-37.23 14.29L-37.59 13.68L-37.7 12.98L-37.58 12.21L-37.21 11.38L-36.61 10.5L-35.81 9.59L-34.81 8.68L-33.65 7.77L-32.37 6.88L-30.98 6.02L-29.54 5.21L-28.09 4.45L-26.65 3.75L-25.27 3.1L-23.99 2.52L-22.84 2L-21.86 1.53L-21.06 1.1L-20.48 0.72L-20.12 0.35L-20 0L-20.12 -0.35L-20.48 -0.72L-21.06 -1.1L-21.86 -1.53L-22.84 -2L-23.99 -2.52L-25.27 -3.1L-26.65 -3.75L-28.09 -4.45L-29.54 -5.21L-30.98 -6.02L-32.37 -6.88L-33.65 -7.77L-34.81 -8.68L-35.81 -9.59L-36.61 -10.5L-37.21 -11.38L-37.58 -12.21L-37.7 -12.98L-37.59 -13.68L-37.23 -14.29L-36.63 -14.8L-35.82 -15.2L-34.8 -15.49L-33.6 -15.67L-32.25 -15.73L-30.78 -15.68L-29.22 -15.53L-27.61 -15.3L-25.98 -15L-24.37 -14.65L-22.82 -14.26L-21.35 -13.87L-20 -13.49L-18.78 -13.15L-17.73 -12.88L-16.84 -12.69L-16.15 -12.61L-15.64 -12.66L-15.32 -12.86L-15.19 -13.2L-15.23 -13.71L-15.42 -14.38L-15.76 -15.22L-16.21 -16.21L-16.76 -17.35L-17.36 -18.62L-18.01 -20L-18.66 -21.46L-19.28 -22.98L-19.86 -24.53L-20.37 -26.08L-20.79 -27.58L-21.09 -29.03L-21.26 -30.37L-21.3 -31.58L-21.19 -32.63L-20.94 -33.51L-20.54 -34.18L-20 -34.64L-19.33 -34.88L-18.55 -34.89L-17.66 -34.67L-16.7 -34.24L-15.67 -33.6L-14.59 -32.78L-13.5 -31.79L-12.4 -30.68L-11.31 -29.47L-10.26 -28.19L-9.26 -26.89L-8.32 -25.59L-7.44 -24.35L-6.65 -23.19L-5.93 -22.15L-5.3 -21.26L-4.74 -20.55L-4.26 -20.04L-3.84 -19.75L-3.47 -19.7L-3.15 -19.88L-2.85 -20.29L-2.57 -20.93L-2.29 -21.79L-2 -22.84L-1.68 -24.06L-1.33 -25.43L-0.94 -26.89L-0.5 -28.43L0 -30L0.55 -31.56L1.15 -33.07L1.81 -34.49L2.5 -35.79L3.23 -36.93L3.98 -37.88L4.74 -38.62L5.5 -39.13L6.24 -39.39L6.95 -39.39L7.61 -39.14L8.21 -38.65L8.75 -37.91L9.21 -36.96L9.59 -35.81L9.89 -34.49L10.1 -33.03L10.23 -31.47L10.28 -29.84L10.26 -28.19L10.19 -26.55L10.08 -24.95L9.95 -23.44L9.81 -22.04L9.69 -20.78L9.6 -19.69L9.57 -18.79L9.62 -18.09L9.76 -17.6L10 -17.32L10.36 -17.25L10.86 -17.38L11.49 -17.69L12.25 -18.16L13.15 -18.78L14.18 -19.52L15.32 -20.33L16.57 -21.21L17.9 -22.1L19.28 -22.98L20.71 -23.82L22.14 -24.59L23.56 -25.26L24.92 -25.81L26.21 -26.21L27.4 -26.46L28.46 -26.54L29.36 -26.44L30.1 -26.16L30.64 -25.71L30.99 -25.1L31.13 -24.33L31.07 -23.42L30.82 -22.39L30.37 -21.26L29.74 -20.06L28.97 -18.81L28.06 -17.54L27.06 -16.26L25.98 -15L24.87 -13.79L23.76 -12.63L22.69 -11.56L21.68 -10.57L20.78 -9.69L20.02 -8.91L19.41 -8.24L19 -7.68L18.79 -7.21L18.79 -6.84L19.03 -6.55L19.49 -6.33L20.17 -6.17L21.06 -6.04L22.15 -5.93L23.41 -5.84L24.81 -5.73L26.32 -5.59L27.91 -5.43L29.54 -5.21L31.18 -4.94L32.77 -4.61L34.28 -4.21L35.68 -3.75L36.93 -3.23L38 -2.66L38.86 -2.04L39.49 -1.38L39.87 -0.7Z"/>
|
||||
<path d="M37.65 0L36.54 0.64L35.26 1.23L33.84 1.77L32.32 2.26L30.74 2.69L29.12 3.06L27.53 3.38L25.98 3.65L24.53 3.88L23.2 4.09L22.03 4.28L21.04 4.47L20.25 4.68L19.69 4.91L19.35 5.19L19.25 5.52L19.38 5.92L19.72 6.41L20.27 6.98L21 7.64L21.9 8.4L22.92 9.26L24.03 10.2L25.21 11.23L26.42 12.32L27.61 13.47L28.75 14.65L29.81 15.85L30.76 17.05L31.56 18.22L32.19 19.34L32.64 20.4L32.89 21.36L32.92 22.2L32.74 22.92L32.34 23.5L31.74 23.92L30.94 24.17L29.97 24.27L28.84 24.2L27.58 23.98L26.22 23.61L24.79 23.11L23.31 22.51L21.82 21.82L20.34 21.07L18.91 20.28L17.56 19.5L16.29 18.74L15.14 18.05L14.12 17.44L13.24 16.95L12.51 16.6L11.93 16.42L11.49 16.41L11.2 16.6L11.03 16.99L10.99 17.58L11.04 18.38L11.18 19.36L11.37 20.51L11.6 21.82L11.85 23.26L12.1 24.81L12.32 26.42L12.49 28.06L12.61 29.7L12.65 31.3L12.6 32.83L12.46 34.24L12.23 35.51L11.89 36.61L11.46 37.5L10.94 38.17L10.34 38.6L9.67 38.79L8.94 38.72L8.16 38.41L7.36 37.85L6.54 37.08L5.72 36.1L4.91 34.94L4.13 33.64L3.39 32.22L2.69 30.74L2.04 29.21L1.45 27.69L0.92 26.22L0.43 24.83L0 23.56L-0.39 22.44L-0.75 21.5L-1.09 20.76L-1.42 20.24L-1.75 19.96L-2.09 19.92L-2.47 20.11L-2.89 20.53L-3.35 21.17L-3.88 22.01L-4.48 23.02L-5.14 24.18L-5.87 25.44L-6.68 26.78L-7.54 28.15L-8.47 29.53L-9.43 30.86L-10.43 32.11L-11.45 33.25L-12.46 34.24L-13.46 35.06L-14.42 35.69L-15.32 36.1L-16.15 36.27L-16.89 36.22L-17.52 35.93L-18.04 35.41L-18.43 34.67L-18.7 33.73L-18.82 32.6L-18.82 31.33L-18.7 29.92L-18.46 28.42L-18.12 26.86L-17.7 25.27L-17.21 23.69L-16.69 22.15L-16.15 20.67L-15.63 19.3L-15.14 18.05L-14.72 16.94L-14.39 15.98L-14.18 15.2L-14.1 14.6L-14.17 14.17L-14.41 13.91L-14.82 13.82L-15.41 13.87L-16.18 14.06L-17.12 14.37L-18.23 14.76L-19.48 15.22L-20.85 15.71L-22.33 16.22L-23.88 16.72L-25.46 17.18L-27.06 17.57L-28.63 17.89L-30.14 18.11L-31.56 18.22L-32.85 18.21L-33.98 18.07L-34.94 17.8L-35.69 17.41L-36.22 16.89L-36.52 16.26L-36.58 15.53L-36.41 14.71L-36 13.82L-35.38 12.88L-34.56 11.9L-33.56 10.9L-32.41 9.91L-31.15 8.93L-29.8 7.99L-28.41 7.08L-27.02 6.24L-25.66 5.45L-24.38 4.74L-23.2 4.09L-22.16 3.51L-21.3 2.99L-20.63 2.53L-20.18 2.12L-19.96 1.75L-19.98 1.4L-20.23 1.06L-20.72 0.72L-21.43 0.37L-22.35 0L-23.45 -0.41L-24.7 -0.86L-26.07 -1.37L-27.53 -1.93L-29.04 -2.54L-30.55 -3.21L-32.03 -3.93L-33.43 -4.7L-34.73 -5.5L-35.89 -6.33L-36.87 -7.17L-37.65 -8L-38.21 -8.82L-38.53 -9.61L-38.6 -10.34L-38.43 -11.02L-38 -11.62L-37.34 -12.13L-36.46 -12.55L-35.38 -12.88L-34.12 -13.1L-32.71 -13.22L-31.2 -13.24L-29.6 -13.18L-27.96 -13.04L-26.32 -12.84L-24.71 -12.59L-23.17 -12.32L-21.72 -12.04L-20.4 -11.78L-19.24 -11.56L-18.24 -11.4L-17.43 -11.32L-16.82 -11.35L-16.41 -11.49L-16.2 -11.77L-16.18 -12.19L-16.34 -12.77L-16.66 -13.49L-17.12 -14.37L-17.7 -15.39L-18.37 -16.54L-19.1 -17.81L-19.85 -19.17L-20.61 -20.61L-21.34 -22.1L-22.01 -23.6L-22.59 -25.09L-23.07 -26.54L-23.42 -27.92L-23.64 -29.19L-23.7 -30.33L-23.6 -31.32L-23.34 -32.12L-22.92 -32.74L-22.35 -33.14L-21.64 -33.33L-20.81 -33.3L-19.86 -33.05L-18.82 -32.6L-17.72 -31.96L-16.56 -31.15L-15.39 -30.2L-14.2 -29.12L-13.04 -27.96L-11.91 -26.75L-10.84 -25.53L-9.83 -24.33L-8.9 -23.18L-8.06 -22.14L-7.31 -21.22L-6.65 -20.46L-6.08 -19.88L-5.59 -19.51L-5.19 -19.35L-4.84 -19.43L-4.56 -19.74L-4.31 -20.28L-4.09 -21.04L-3.88 -22.01L-3.67 -23.16L-3.44 -24.48L-3.18 -25.92L-2.88 -27.45L-2.54 -29.04L-2.14 -30.64L-1.69 -32.22L-1.18 -33.74L-0.61 -35.16L0 -36.44L0.66 -37.55L1.34 -38.47L2.05 -39.16L2.77 -39.61L3.48 -39.81L4.18 -39.76L4.84 -39.44L5.46 -38.88L6.03 -38.09L6.54 -37.08L6.97 -35.88L7.34 -34.51L7.62 -33.02L7.84 -31.44L7.99 -29.8L8.07 -28.15L8.11 -26.52L8.11 -24.95L8.09 -23.48L8.06 -22.14L8.04 -20.95L8.06 -19.94L8.12 -19.14L8.25 -18.54L8.47 -18.16L8.78 -18L9.2 -18.05L9.73 -18.31L10.39 -18.75L11.18 -19.36L12.08 -20.1L13.1 -20.96L14.22 -21.9L15.43 -22.88L16.72 -23.88L18.05 -24.85L19.42 -25.77L20.79 -26.61L22.13 -27.33L23.42 -27.92L24.64 -28.35L25.76 -28.6L26.74 -28.68L27.58 -28.56L28.26 -28.26L28.76 -27.77L29.06 -27.1L29.18 -26.27L29.1 -25.3L28.84 -24.2L28.4 -23L27.8 -21.72L27.07 -20.4L26.21 -19.04L25.27 -17.7L24.28 -16.38L23.26 -15.1L22.25 -13.9L21.29 -12.79L20.4 -11.78L19.63 -10.88L18.99 -10.1L18.52 -9.44L18.24 -8.9L18.16 -8.47L18.29 -8.15L18.65 -7.92L19.23 -7.77L20.01 -7.68L21 -7.64L22.18 -7.64L23.51 -7.64L24.97 -7.63L26.53 -7.61L28.15 -7.54L29.8 -7.43L31.44 -7.26L33.03 -7.02L34.52 -6.71L35.89 -6.33L37.1 -5.88L38.12 -5.36L38.92 -4.78L39.49 -4.15L39.81 -3.48L39.88 -2.79L39.69 -2.08L39.24 -1.37L38.56 -0.67Z"/>
|
||||
<path d="M31.7 0L30.13 0.53L28.55 1L27 1.42L25.52 1.78L24.14 2.11L22.9 2.41L21.83 2.68L20.95 2.94L20.28 3.21L19.84 3.5L19.63 3.82L19.66 4.18L19.92 4.6L20.4 5.09L21.09 5.65L21.95 6.29L22.96 7.02L24.1 7.83L25.32 8.72L26.59 9.68L27.88 10.7L29.14 11.77L30.34 12.88L31.44 14L32.42 15.12L33.23 16.21L33.87 17.26L34.3 18.24L34.52 19.13L34.52 19.93L34.29 20.6L33.83 21.14L33.17 21.54L32.31 21.79L31.27 21.89L30.07 21.85L28.74 21.66L27.31 21.34L25.82 20.91L24.28 20.38L22.74 19.77L21.23 19.12L19.78 18.44L18.4 17.77L17.14 17.14L16 16.56L15 16.08L14.15 15.72L13.47 15.5L12.95 15.43L12.59 15.54L12.38 15.84L12.31 16.33L12.36 17.01L12.52 17.88L12.77 18.93L13.08 20.14L13.43 21.49L13.79 22.95L14.15 24.51L14.48 26.12L14.75 27.75L14.96 29.37L15.09 30.93L15.12 32.42L15.04 33.78L14.85 34.99L14.55 36.02L14.14 36.85L13.63 37.45L13.02 37.82L12.33 37.94L11.56 37.82L10.74 37.46L9.88 36.87L8.99 36.06L8.1 35.07L7.21 33.9L6.34 32.61L5.5 31.22L4.71 29.77L3.98 28.29L3.3 26.84L2.67 25.44L2.11 24.14L1.61 22.97L1.15 21.96L0.74 21.14L0.36 20.53L0 20.15L-0.35 20L-0.7 20.09L-1.07 20.42L-1.47 20.98L-1.9 21.75L-2.39 22.71L-2.93 23.83L-3.53 25.09L-4.19 26.45L-4.91 27.87L-5.7 29.31L-6.53 30.74L-7.41 32.11L-8.33 33.39L-9.26 34.55L-10.19 35.54L-11.11 36.35L-12 36.95L-12.85 37.32L-13.63 37.45L-14.33 37.34L-14.95 36.99L-15.45 36.41L-15.85 35.6L-16.13 34.59L-16.29 33.41L-16.34 32.07L-16.27 30.6L-16.11 29.06L-15.85 27.45L-15.52 25.83L-15.14 24.23L-14.73 22.68L-14.31 21.21L-13.9 19.85L-13.53 18.63L-13.23 17.56L-13.02 16.67L-12.92 15.96L-12.95 15.43L-13.12 15.09L-13.45 14.94L-13.95 14.95L-14.61 15.13L-15.44 15.44L-16.42 15.86L-17.56 16.38L-18.83 16.95L-20.21 17.57L-21.68 18.19L-23.21 18.79L-24.77 19.35L-26.32 19.84L-27.84 20.23L-29.3 20.51L-30.65 20.68L-31.88 20.7L-32.94 20.59L-33.83 20.33L-34.52 19.93L-34.98 19.39L-35.23 18.73L-35.24 17.96L-35.03 17.08L-34.59 16.13L-33.95 15.12L-33.13 14.06L-32.14 12.98L-31.01 11.91L-29.79 10.84L-28.5 9.81L-27.17 8.83L-25.86 7.91L-24.59 7.05L-23.41 6.27L-22.34 5.57L-21.43 4.95L-20.69 4.4L-20.16 3.92L-19.84 3.5L-19.75 3.13L-19.91 2.8L-20.3 2.49L-20.91 2.2L-21.75 1.9L-22.78 1.59L-23.98 1.26L-25.32 0.88L-26.78 0.47L-28.3 0L-29.86 -0.52L-31.41 -1.1L-32.91 -1.72L-34.33 -2.4L-35.63 -3.12L-36.77 -3.86L-37.73 -4.63L-38.47 -5.41L-38.98 -6.17L-39.25 -6.92L-39.26 -7.63L-39.03 -8.3L-38.54 -8.9L-37.81 -9.43L-36.87 -9.88L-35.73 -10.24L-34.42 -10.52L-32.97 -10.71L-31.41 -10.82L-29.79 -10.84L-28.14 -10.8L-26.49 -10.7L-24.89 -10.57L-23.37 -10.41L-21.96 -10.24L-20.7 -10.09L-19.59 -9.98L-18.68 -9.93L-17.96 -9.95L-17.45 -10.07L-17.14 -10.3L-17.05 -10.65L-17.15 -11.14L-17.43 -11.76L-17.88 -12.52L-18.47 -13.42L-19.18 -14.45L-19.97 -15.6L-20.81 -16.85L-21.68 -18.19L-22.54 -19.59L-23.36 -21.03L-24.11 -22.48L-24.76 -23.91L-25.29 -25.29L-25.68 -26.6L-25.92 -27.8L-25.99 -28.87L-25.89 -29.79L-25.62 -30.53L-25.17 -31.09L-24.56 -31.44L-23.8 -31.59L-22.91 -31.53L-21.89 -31.27L-20.78 -30.81L-19.6 -30.18L-18.37 -29.39L-17.11 -28.48L-15.85 -27.45L-14.61 -26.36L-13.41 -25.23L-12.28 -24.09L-11.21 -22.99L-10.24 -21.96L-9.37 -21.04L-8.59 -20.24L-7.92 -19.61L-7.36 -19.17L-6.89 -18.93L-6.51 -18.91L-6.21 -19.12L-5.98 -19.55L-5.8 -20.21L-5.65 -21.09L-5.52 -22.15L-5.4 -23.4L-5.27 -24.78L-5.11 -26.29L-4.91 -27.87L-4.67 -29.5L-4.37 -31.12L-4.02 -32.71L-3.6 -34.23L-3.12 -35.63L-2.58 -36.88L-1.99 -37.96L-1.36 -38.82L-0.69 -39.46L0 -39.85L0.7 -39.99L1.39 -39.87L2.07 -39.5L2.72 -38.88L3.33 -38.02L3.89 -36.96L4.39 -35.72L4.82 -34.32L5.2 -32.81L5.5 -31.22L5.75 -29.58L5.94 -27.95L6.08 -26.35L6.19 -24.82L6.27 -23.41L6.35 -22.13L6.43 -21.03L6.54 -20.12L6.68 -19.41L6.89 -18.93L7.17 -18.67L7.53 -18.64L7.99 -18.82L8.55 -19.21L9.23 -19.78L10.01 -20.52L10.9 -21.39L11.9 -22.37L12.98 -23.42L14.15 -24.51L15.38 -25.6L16.65 -26.65L17.95 -27.64L19.25 -28.53L20.51 -29.3L21.73 -29.91L22.87 -30.36L23.92 -30.61L24.84 -30.67L25.62 -30.53L26.24 -30.19L26.7 -29.65L26.97 -28.93L27.07 -28.03L26.99 -26.99L26.74 -25.82L26.32 -24.54L25.76 -23.19L25.07 -21.79L24.28 -20.38L23.42 -18.97L22.51 -17.59L21.6 -16.27L20.7 -15.04L19.85 -13.9L19.09 -12.88L18.44 -11.98L17.94 -11.21L17.6 -10.58L17.45 -10.07L17.49 -9.7L17.75 -9.44L18.22 -9.28L18.9 -9.22L19.78 -9.23L20.86 -9.29L22.1 -9.38L23.49 -9.49L25 -9.6L26.59 -9.68L28.24 -9.72L29.89 -9.71L31.52 -9.64L33.08 -9.49L34.55 -9.26L35.88 -8.94L37.03 -8.55L38 -8.08L38.74 -7.53L39.25 -6.92L39.51 -6.26L39.51 -5.55L39.26 -4.82L38.76 -4.07L38.02 -3.33L37.08 -2.59L35.94 -1.88L34.64 -1.21L33.22 -0.58Z"/>
|
||||
</g>
|
||||
<g font-family="Cinzel, serif" font-weight="600" fill="rgba(70,45,10,.85)" text-anchor="middle">
|
||||
<text font-size="8.6" letter-spacing="1.5"><textPath href="#seal-top" startOffset="50%">INDEPENDENT PROOF LLC</textPath></text>
|
||||
<text font-size="8.6" letter-spacing="2.4"><textPath href="#seal-bottom" startOffset="50%">MMXXVI</textPath></text>
|
||||
</g>
|
||||
<g fill="rgba(70,45,10,.75)">
|
||||
<circle cx="-38.9" cy="38.9" r="1.6"/>
|
||||
<circle cx="38.9" cy="38.9" r="1.6"/>
|
||||
</g>
|
||||
<text y="7" text-anchor="middle" font-family="Cinzel, serif" font-size="20" font-weight="600" fill="rgba(70,45,10,.9)">IP</text>
|
||||
</svg>
|
||||
<div class="sig">In preparation</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer>© 2026 Independent Proof LLC</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env node
|
||||
// Builds the site into dist/. Today the site is one hand-written page, so the build is a clean copy
|
||||
// of public/. dist/ is what every deploy sends, so a later static-site generator only has to keep
|
||||
// writing its output there.
|
||||
import { cpSync, rmSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const dist = join(root, 'dist');
|
||||
rmSync(dist, { recursive: true, force: true });
|
||||
cpSync(join(root, 'public'), dist, { recursive: true });
|
||||
console.log('built dist/');
|
||||
@@ -0,0 +1,445 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Read and write files in NextExplorer, and publish the site to the folder Start9 Pages serves.
|
||||
*
|
||||
* node scripts/nextexplorer.mjs <command> [args] (or: npm run nx -- <command> [args])
|
||||
*
|
||||
* whoami the account, and the locations it can reach, with the path
|
||||
* Start9 Pages needs for each
|
||||
* ls [path] list a folder (no path: the account's locations)
|
||||
* get <remote-file> [local-file] download a file
|
||||
* put <local> <remote-folder> upload a file, or a folder's contents, into a folder
|
||||
* (--replace: overwrite names that already exist)
|
||||
* mkdir <path> create a folder, and any missing parents
|
||||
* mv <remote> <new-name> rename in place
|
||||
* rm <remote> [--permanent] delete (to NextExplorer's trash, unless --permanent)
|
||||
* deploy [--dry-run] [--no-build] build the site and publish it to NX_DEST
|
||||
* rollback swap NX_DEST with the copy the last deploy kept
|
||||
*
|
||||
* Remote paths start with where the account's access comes from, as `whoami` shows it:
|
||||
* - a location (granted in the account's Volumes tab): Websites/independentproof/index.html
|
||||
* - a folder shared with the account, by the share's label: share:Websites/independentproof/index.html
|
||||
* If several shares carry that label, the read-write one is used.
|
||||
*
|
||||
* SETTINGS come from deploy.local.env (git-ignored) and the environment, the environment winning:
|
||||
* NX_URL NextExplorer address, e.g. https://<server>.local:<port>
|
||||
* NX_USER account email
|
||||
* NX_PASS the password, or NX_PASS_FILE: a file holding only the password
|
||||
* (default ~/.config/nextexplorer/<account name>.password, which should be mode 600)
|
||||
* NX_CA_FILE the StartOS server's root CA, so TLS is verified rather than switched off
|
||||
* NX_DEST the folder the site is published to, e.g. share:Websites/independentproof
|
||||
* NX_PAGES_PATH the same folder as Start9 Pages names it (from the storage root, e.g.
|
||||
* Files/Websites/independentproof). Needed for a share, whose location on disk an account
|
||||
* that doesn't own it can't see; worked out automatically for a location.
|
||||
* SITE_URL where Start9 Pages serves the site; printed after a deploy
|
||||
*
|
||||
* WHY DEPLOY SWAPS FOLDERS. NextExplorer never overwrites on upload: a second index.html lands as
|
||||
* "index (1).html". So deploy uploads the whole build into a fresh sibling folder, then renames the
|
||||
* live folder aside to "<name>.previous" and the new one into place. Stale files can't linger, the
|
||||
* live site is never half-updated, and `rollback` can swap the previous copy back.
|
||||
*
|
||||
* API: read from NextExplorer 3.1.0's backend (backend/src/routes). Login is a session cookie from
|
||||
* POST /api/auth/login; uploads are multipart POST /api/upload with `uploadTo` (an existing folder)
|
||||
* and `relativePath` (may include new subfolders) sent before the file part, one file per request.
|
||||
*/
|
||||
|
||||
import { execFileSync, spawnSync } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { basename, dirname, join, posix, relative, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const dist = join(root, 'dist');
|
||||
|
||||
// ---------------------------------------------------------------------------- settings
|
||||
|
||||
const localEnv = join(root, 'deploy.local.env');
|
||||
if (existsSync(localEnv)) {
|
||||
for (const line of readFileSync(localEnv, 'utf8').split('\n')) {
|
||||
const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$/);
|
||||
if (m && process.env[m[1]] === undefined) process.env[m[1]] = m[2].replace(/^(['"])(.*)\1$/, '$2');
|
||||
}
|
||||
}
|
||||
const env = process.env;
|
||||
const expand = (p) => (p ? p.replace(/^~(?=$|\/)/, homedir()) : p);
|
||||
|
||||
// Node reads extra CA certificates only at startup, so re-run once with the server's CA added.
|
||||
const caFile = expand(env.NX_CA_FILE);
|
||||
if (caFile && env.NODE_EXTRA_CA_CERTS !== caFile) {
|
||||
if (!existsSync(caFile)) fail(`NX_CA_FILE not found: ${caFile}`);
|
||||
const r = spawnSync(process.execPath, process.argv.slice(1), {
|
||||
stdio: 'inherit',
|
||||
env: { ...env, NODE_EXTRA_CA_CERTS: caFile },
|
||||
});
|
||||
process.exit(r.status ?? 1);
|
||||
}
|
||||
|
||||
const BASE = (env.NX_URL ?? '').replace(/\/+$/, '');
|
||||
|
||||
function fail(msg) {
|
||||
console.error(`error: ${msg}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function password() {
|
||||
if (env.NX_PASS) return env.NX_PASS;
|
||||
const account = (env.NX_USER ?? '').split('@')[0];
|
||||
const file = expand(env.NX_PASS_FILE) || join(homedir(), '.config/nextexplorer', `${account}.password`);
|
||||
if (!existsSync(file)) {
|
||||
fail(`no password: set NX_PASS, or put it (and nothing else) in ${file} with mode 600`);
|
||||
}
|
||||
if ((statSync(file).mode & 0o077) !== 0) console.warn(`! ${file} is readable by others; chmod 600 it`);
|
||||
return readFileSync(file, 'utf8').replace(/\r?\n$/, '');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------- API
|
||||
|
||||
let cookie = '';
|
||||
|
||||
async function api(method, path, { json, form, raw } = {}) {
|
||||
const headers = { cookie };
|
||||
let body;
|
||||
if (json !== undefined) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
body = JSON.stringify(json);
|
||||
} else if (form) {
|
||||
body = form;
|
||||
}
|
||||
const res = await fetch(`${BASE}${path}`, { method, headers, body });
|
||||
if (raw) {
|
||||
if (!res.ok) throw new Error(`${method} ${path}: ${res.status} ${await errorText(res)}`);
|
||||
return res;
|
||||
}
|
||||
const text = await res.text();
|
||||
if (!res.ok) throw new Error(`${method} ${path}: ${res.status} ${errorOf(text)}`);
|
||||
return text ? JSON.parse(text) : null;
|
||||
}
|
||||
|
||||
const errorOf = (text) => {
|
||||
try {
|
||||
const j = JSON.parse(text);
|
||||
return j.error?.message ?? j.error ?? j.message ?? text;
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
};
|
||||
const errorText = async (res) => errorOf(await res.text());
|
||||
|
||||
/**
|
||||
* The session is saved and reused (it lasts 30 days). NextExplorer allows only 10 logins per 15
|
||||
* minutes per address, so logging in on every command would lock the account out mid-deploy.
|
||||
*/
|
||||
function sessionFile() {
|
||||
const account = (env.NX_USER ?? '').split('@')[0];
|
||||
return join(homedir(), '.config/nextexplorer', `${account}.session`);
|
||||
}
|
||||
|
||||
async function login() {
|
||||
if (!BASE) fail('NX_URL is not set (deploy.local.env)');
|
||||
if (!env.NX_USER) fail('NX_USER is not set (deploy.local.env)');
|
||||
|
||||
const saved = sessionFile();
|
||||
if (existsSync(saved)) {
|
||||
cookie = readFileSync(saved, 'utf8').trim();
|
||||
const me = await fetch(`${BASE}/api/auth/me`, { headers: { cookie } });
|
||||
if (me.ok) {
|
||||
const body = await me.json();
|
||||
const user = body.user ?? body;
|
||||
if (user?.email === env.NX_USER || user?.username === env.NX_USER.split('@')[0]) return user;
|
||||
}
|
||||
cookie = '';
|
||||
}
|
||||
|
||||
const res = await fetch(`${BASE}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: env.NX_USER, password: password() }),
|
||||
});
|
||||
if (!res.ok) fail(`login failed: ${res.status} ${await errorText(res)}`);
|
||||
const cookies = res.headers.getSetCookie();
|
||||
if (cookies.length === 0) fail('login succeeded but set no session cookie');
|
||||
cookie = cookies.map((c) => c.split(';')[0]).join('; ');
|
||||
mkdirSync(dirname(saved), { recursive: true, mode: 0o700 });
|
||||
writeFileSync(saved, cookie, { mode: 0o600 });
|
||||
return (await res.json()).user;
|
||||
}
|
||||
|
||||
const clean = (p) => (p ?? '').replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||
|
||||
/** Folders shared with this account: [{ label, shareToken, accessMode, ... }]. */
|
||||
let sharesCache;
|
||||
async function sharedWithMe() {
|
||||
sharesCache ??= (await api('GET', '/api/shares/shared-with-me')).shares ?? [];
|
||||
return sharesCache;
|
||||
}
|
||||
|
||||
/** `share:<label>/rest` → NextExplorer's own `share/<token>/rest`. Other paths pass through. */
|
||||
async function resolve(path) {
|
||||
const p = clean(path);
|
||||
const m = p.match(/^share:([^/]+)(\/.*)?$/);
|
||||
if (!m) return p;
|
||||
const matches = (await sharedWithMe()).filter((s) => s.label === m[1]);
|
||||
if (!matches.length) throw new Error(`no share labelled "${m[1]}" (see whoami)`);
|
||||
const share = matches.find((s) => s.accessMode === 'readwrite') ?? matches[0];
|
||||
return clean(`share/${share.shareToken}${m[2] ?? ''}`);
|
||||
}
|
||||
|
||||
/** How many leading segments name the root of access, which an account can't create. */
|
||||
const rootDepth = (p) => (p.startsWith('share/') ? 2 : 1);
|
||||
const encodePath = (p) => clean(p).split('/').filter(Boolean).map(encodeURIComponent).join('/');
|
||||
|
||||
/** Items in a folder: [{ name, kind ('directory' or an extension), size, dateModified }]. */
|
||||
async function list(path) {
|
||||
const r = await api('GET', `/api/browse/${encodePath(path)}`);
|
||||
return r.items ?? r;
|
||||
}
|
||||
|
||||
/** The item at `path`, or null. The parent must exist. */
|
||||
async function stat(path) {
|
||||
const p = clean(path);
|
||||
const parent = posix.dirname(p);
|
||||
const items = await list(parent === '.' ? '' : parent);
|
||||
return items.find((i) => i.name === posix.basename(p)) ?? null;
|
||||
}
|
||||
|
||||
async function exists(path) {
|
||||
try {
|
||||
return (await stat(path)) !== null;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function mkdirp(path) {
|
||||
const parts = clean(path).split('/');
|
||||
// The first segment(s) name a location or share, which an account can't create.
|
||||
for (let i = rootDepth(clean(path)) + 1; i <= parts.length; i++) {
|
||||
const p = parts.slice(0, i).join('/');
|
||||
if (await exists(p)) continue;
|
||||
// NextExplorer picks "name 2" rather than fail when a name is taken, so check what it made.
|
||||
const r = await api('POST', '/api/files/folder', { json: { path: posix.dirname(p), name: posix.basename(p) } });
|
||||
if (r?.item?.name && r.item.name !== posix.basename(p)) throw new Error(`created "${r.item.name}", not "${p}"`);
|
||||
}
|
||||
}
|
||||
|
||||
async function rename(path, newName) {
|
||||
const p = clean(path);
|
||||
await api('POST', '/api/files/rename', {
|
||||
json: { path: posix.dirname(p), name: posix.basename(p), newName },
|
||||
});
|
||||
}
|
||||
|
||||
async function remove(path, permanent) {
|
||||
const p = clean(path);
|
||||
await api('DELETE', '/api/files', {
|
||||
json: { items: [{ path: posix.dirname(p), name: posix.basename(p) }], permanent },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload one local file to `<uploadTo>/<relativePath>`. uploadTo must exist; folders inside
|
||||
* relativePath are created. Fails rather than accept the "name (1)" a taken name would get.
|
||||
*/
|
||||
async function uploadFile(localPath, uploadTo, relativePath) {
|
||||
const form = new FormData();
|
||||
// Text fields first: the server reads them while the file part is still streaming in.
|
||||
form.append('uploadTo', clean(uploadTo));
|
||||
form.append('relativePath', clean(relativePath));
|
||||
form.append('filedata', new Blob([readFileSync(localPath)]), posix.basename(relativePath));
|
||||
const [placed] = await api('POST', '/api/upload', { form });
|
||||
const want = posix.basename(relativePath);
|
||||
if (placed?.name !== want) {
|
||||
throw new Error(`"${relativePath}" was stored as "${placed?.name}" because the name was taken`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Run `task` over `items`, `n` at a time. */
|
||||
async function pool(items, n, task) {
|
||||
let next = 0;
|
||||
await Promise.all(
|
||||
Array.from({ length: Math.min(n, items.length) }, async () => {
|
||||
while (next < items.length) await task(items[next++]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** Every file under `dir`, relative, with POSIX separators. */
|
||||
function walk(dir, base = dir) {
|
||||
const out = [];
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const full = join(dir, entry);
|
||||
if (statSync(full).isDirectory()) out.push(...walk(full, base));
|
||||
else out.push(relative(base, full).split(sep).join('/'));
|
||||
}
|
||||
return out.sort();
|
||||
}
|
||||
|
||||
/** The path Start9 Pages needs for a NextExplorer path: relative to the storage root, /mnt. */
|
||||
async function pagesPath(path) {
|
||||
if (clean(path).startsWith('share/')) return null; // not visible to an account that doesn't own it
|
||||
const [first, ...rest] = clean(path).split('/');
|
||||
const vol = (await api('GET', '/api/volumes')).find((v) => v.name === first);
|
||||
if (!vol?.actualPath) return null;
|
||||
const onDisk = vol.actualPath.replace(/^\/mnt\/?/, '');
|
||||
if (onDisk.startsWith('_users')) return null; // a personal space, which Start9 Pages can't be pointed at
|
||||
return [onDisk, ...rest].filter(Boolean).join('/');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------- commands
|
||||
|
||||
const [cmd, ...args] = process.argv.slice(2);
|
||||
const flags = new Set(args.filter((a) => a.startsWith('--')));
|
||||
const pos = args.filter((a) => !a.startsWith('--'));
|
||||
|
||||
const commands = {
|
||||
async whoami() {
|
||||
const user = await login();
|
||||
console.log(`${user.email ?? user.username} (${(user.roles ?? []).join(', ') || 'user'}) at ${BASE}\n`);
|
||||
const vols = await api('GET', '/api/volumes');
|
||||
const shares = await sharedWithMe();
|
||||
if (!vols.length && !shares.length) console.log('No locations or shares. An admin grants a location in the account\'s Volumes tab, or shares a folder with it.');
|
||||
if (vols.length) console.log('Locations:');
|
||||
for (const v of vols) {
|
||||
const pages = await pagesPath(v.name);
|
||||
console.log(` ${v.name.padEnd(28)} Start9 Pages path: ${pages ?? '(personal space; not servable)'}`);
|
||||
}
|
||||
if (shares.length) console.log('Shared with this account (use as share:<label>/...):');
|
||||
for (const sh of shares) {
|
||||
console.log(` share:${sh.label.padEnd(22)} ${sh.accessMode.padEnd(10)} token ${sh.shareToken}${sh.expiresAt ? `, expires ${sh.expiresAt}` : ''}`);
|
||||
}
|
||||
},
|
||||
|
||||
async ls() {
|
||||
await login();
|
||||
const path = pos[0] ? await resolve(pos[0]) : '';
|
||||
const items = path ? await list(path) : await api('GET', '/api/volumes');
|
||||
for (const i of items) {
|
||||
const dir = !path || i.kind === 'directory';
|
||||
console.log(`${dir ? 'd' : '-'} ${dir ? ''.padStart(10) : String(i.size).padStart(10)} ${i.name}${dir ? '/' : ''}`);
|
||||
}
|
||||
},
|
||||
|
||||
async get() {
|
||||
const [remote, local = posix.basename(clean(pos[0] ?? ''))] = pos;
|
||||
if (!remote) fail('usage: get <remote-file> [local-file]');
|
||||
await login();
|
||||
const res = await api('POST', '/api/download', { json: { path: await resolve(remote) }, raw: true });
|
||||
mkdirSync(dirname(local) || '.', { recursive: true });
|
||||
writeFileSync(local, Buffer.from(await res.arrayBuffer()));
|
||||
console.log(`${remote} → ${local}`);
|
||||
},
|
||||
|
||||
async put() {
|
||||
const [local, remoteArg] = pos;
|
||||
if (!local || !remoteArg) fail('usage: put <local-file-or-folder> <remote-folder> [--replace]');
|
||||
await login();
|
||||
const remoteDir = await resolve(remoteArg);
|
||||
const isDir = statSync(local).isDirectory();
|
||||
const files = isDir ? walk(local) : [basename(local)];
|
||||
const from = isDir ? local : dirname(local);
|
||||
await mkdirp(remoteDir);
|
||||
let n = 0;
|
||||
await pool(files, 4, async (f) => {
|
||||
const target = `${clean(remoteDir)}/${f}`;
|
||||
if (flags.has('--replace') && (await exists(target))) await remove(target, true);
|
||||
await uploadFile(join(from, f), remoteDir, f);
|
||||
console.log(` [${String(++n).padStart(3)}/${files.length}] ${target}`);
|
||||
});
|
||||
},
|
||||
|
||||
async mkdir() {
|
||||
if (!pos[0]) fail('usage: mkdir <path>');
|
||||
await login();
|
||||
await mkdirp(await resolve(pos[0]));
|
||||
console.log(`ok: ${clean(pos[0])}`);
|
||||
},
|
||||
|
||||
async mv() {
|
||||
const [path, newName] = pos;
|
||||
if (!path || !newName) fail('usage: mv <remote> <new-name>');
|
||||
await login();
|
||||
await rename(await resolve(path), newName);
|
||||
console.log(`${clean(path)} → ${newName}`);
|
||||
},
|
||||
|
||||
async rm() {
|
||||
if (!pos[0]) fail('usage: rm <remote> [--permanent]');
|
||||
await login();
|
||||
await remove(await resolve(pos[0]), flags.has('--permanent'));
|
||||
console.log(`deleted${flags.has('--permanent') ? ' permanently' : ' (to trash)'}: ${clean(pos[0])}`);
|
||||
},
|
||||
|
||||
async deploy() {
|
||||
const destArg = clean(env.NX_DEST);
|
||||
if (!destArg.includes('/')) fail('NX_DEST must be a folder inside a location or share, e.g. share:Websites/independentproof');
|
||||
|
||||
if (!flags.has('--no-build')) {
|
||||
console.log('building…');
|
||||
execFileSync('npm', ['run', 'build'], { cwd: root, stdio: 'inherit' });
|
||||
}
|
||||
// .htaccess is for Apache hosting; Start9 Pages' nginx would serve it as a plain file.
|
||||
const files = walk(dist).filter((f) => !f.split('/').some((s) => s.startsWith('.')));
|
||||
if (!files.length) fail('dist/ is empty; nothing to deploy');
|
||||
const kb = files.reduce((n, f) => n + statSync(join(dist, f)).size, 0) / 1024;
|
||||
console.log(`\n${files.length} files, ${kb.toFixed(0)} KB → ${destArg}`);
|
||||
if (flags.has('--dry-run')) {
|
||||
files.forEach((f) => console.log(` ${destArg}/${f}`));
|
||||
console.log('\ndry run: nothing was sent');
|
||||
return;
|
||||
}
|
||||
|
||||
await login();
|
||||
const dest = await resolve(destArg);
|
||||
const parent = posix.dirname(dest);
|
||||
const name = posix.basename(dest);
|
||||
if (parent.split('/').length > rootDepth(parent)) await mkdirp(parent);
|
||||
const stamp = new Date().toISOString().replace(/\D/g, '').slice(0, 14);
|
||||
const staging = `${name}.incoming-${stamp}`;
|
||||
|
||||
let n = 0;
|
||||
await pool(files, 4, async (f) => {
|
||||
await uploadFile(join(dist, f), parent, `${staging}/${f}`);
|
||||
if (++n % 10 === 0 || n === files.length) console.log(` uploaded ${n}/${files.length}`);
|
||||
});
|
||||
|
||||
const previous = `${name}.previous`;
|
||||
if (await exists(`${parent}/${previous}`)) await remove(`${parent}/${previous}`, true);
|
||||
const hadLive = await exists(dest);
|
||||
if (hadLive) await rename(dest, previous);
|
||||
await rename(`${parent}/${staging}`, name);
|
||||
console.log(`\npublished ${destArg}${hadLive ? ` (the last version is kept as ${previous}; \`rollback\` restores it)` : ''}`);
|
||||
|
||||
const pages = env.NX_PAGES_PATH || (await pagesPath(dest));
|
||||
console.log(
|
||||
pages
|
||||
? `Start9 Pages path: ${pages}`
|
||||
: '! set NX_PAGES_PATH to where this folder sits on disk (e.g. Files/Websites/independentproof) to have it printed',
|
||||
);
|
||||
if (env.SITE_URL) console.log(`Site: ${env.SITE_URL}`);
|
||||
},
|
||||
|
||||
async rollback() {
|
||||
await login();
|
||||
const dest = await resolve(env.NX_DEST);
|
||||
const parent = posix.dirname(dest);
|
||||
const name = posix.basename(dest);
|
||||
const previous = `${parent}/${name}.previous`;
|
||||
if (!(await exists(previous))) fail(`nothing to roll back to: ${previous} doesn't exist`);
|
||||
const swap = `${name}.swap-${Date.now()}`;
|
||||
await rename(dest, swap);
|
||||
await rename(previous, name);
|
||||
await rename(`${parent}/${swap}`, `${name}.previous`);
|
||||
console.log(`restored ${clean(env.NX_DEST)} from ${name}.previous (the version it replaced is now ${name}.previous)`);
|
||||
},
|
||||
};
|
||||
|
||||
if (!commands[cmd]) {
|
||||
console.log(readFileSync(fileURLToPath(import.meta.url), 'utf8').match(/\/\*\*([\s\S]*?)\*\//)[1].replace(/^ \* ?/gm, ''));
|
||||
process.exit(cmd ? 1 : 0);
|
||||
}
|
||||
try {
|
||||
await commands[cmd]();
|
||||
} catch (err) {
|
||||
fail(err.cause ? `${err.message}: ${err.cause.code ?? err.cause.message ?? err.cause}` : err.message);
|
||||
}
|
||||
Reference in New Issue
Block a user