nixamp 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +225 -0
- package/bin/nixamp.mjs +4 -1
- package/dist/accounts.d.ts +54 -0
- package/dist/accounts.js +160 -0
- package/dist/admin.d.ts +47 -0
- package/dist/admin.js +209 -0
- package/dist/broadcast.d.ts +96 -0
- package/dist/broadcast.js +193 -0
- package/dist/channels.d.ts +94 -0
- package/dist/channels.js +235 -0
- package/dist/connections.d.ts +72 -0
- package/dist/connections.js +128 -0
- package/dist/daemon.d.ts +39 -0
- package/dist/daemon.js +170 -0
- package/dist/directory.d.ts +63 -0
- package/dist/directory.js +111 -0
- package/dist/ingest.d.ts +80 -0
- package/dist/ingest.js +252 -0
- package/dist/main.js +103 -6
- package/dist/manage.js +30 -7
- package/dist/owner.d.ts +53 -0
- package/dist/owner.js +96 -0
- package/dist/paywall.d.ts +60 -0
- package/dist/paywall.js +162 -0
- package/dist/playlist.d.ts +16 -0
- package/dist/playlist.js +57 -2
- package/dist/publish.d.ts +36 -0
- package/dist/publish.js +90 -0
- package/dist/rtmp-in.d.ts +22 -0
- package/dist/rtmp-in.js +79 -0
- package/dist/server.d.ts +117 -4
- package/dist/server.js +923 -24
- package/dist/session.d.ts +29 -0
- package/dist/session.js +184 -0
- package/dist/share.d.ts +74 -0
- package/dist/share.js +172 -0
- package/dist/sources.d.ts +37 -0
- package/dist/sources.js +125 -0
- package/package.json +5 -2
- package/src/accounts.ts +193 -0
- package/src/admin.ts +243 -0
- package/src/broadcast.ts +264 -0
- package/src/channels.ts +281 -0
- package/src/connections.ts +158 -0
- package/src/daemon.ts +193 -0
- package/src/directory.ts +135 -0
- package/src/ingest.ts +297 -0
- package/src/main.ts +107 -6
- package/src/manage.ts +35 -7
- package/src/owner.ts +113 -0
- package/src/paywall.ts +198 -0
- package/src/playlist.ts +68 -2
- package/src/publish.ts +101 -0
- package/src/rtmp-in.ts +90 -0
- package/src/server.ts +1087 -23
- package/src/session.ts +209 -0
- package/src/share.ts +193 -0
- package/src/sources.ts +136 -0
- package/src/types/auth-system.d.ts +77 -0
- package/web/dist/assets/{index-BGKWWaIx.css → index-0wAv50Ay.css} +1 -1
- package/web/dist/assets/index-WYJ6R4uF.js +1 -0
- package/web/dist/index.html +37 -6
- package/web/dist/install.ps1 +214 -0
- package/web/dist/sw.js +3 -3
- package/web/dist/assets/index-Dhja5wxB.js +0 -1
package/README.md
CHANGED
|
@@ -48,6 +48,18 @@ curl -fsSL https://nixamp.com/install.sh | sh -s -- --version 0.1.0
|
|
|
48
48
|
curl -fsSL https://nixamp.com/install.sh | sh -s -- --prefix ~/opt
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
On Windows, in PowerShell:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
irm https://nixamp.com/install.ps1 | iex
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
That lands under `%LOCALAPPDATA%\nixamp`, adds itself to your user PATH, and
|
|
58
|
+
needs no administrator rights. Nothing is code signed, so SmartScreen will warn
|
|
59
|
+
the first time.
|
|
60
|
+
|
|
61
|
+
Builds are published for Linux, macOS and Windows, on both x64 and arm64.
|
|
62
|
+
|
|
51
63
|
Then:
|
|
52
64
|
|
|
53
65
|
```
|
|
@@ -63,6 +75,219 @@ network. Your music is never touched.
|
|
|
63
75
|
If you would rather not pipe a script into a shell, `npm i -g nixamp` and
|
|
64
76
|
`bunx nixamp ~/Music` both work; that route needs Node 24 or newer.
|
|
65
77
|
|
|
78
|
+
## Signing in
|
|
79
|
+
|
|
80
|
+
An account on nixamp.com is what lets you publish, be paid, and administer a
|
|
81
|
+
server you own. Email and password, on every surface:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
nixamp login # or: nixamp signup
|
|
85
|
+
nixamp whoami
|
|
86
|
+
nixamp logout
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The PWA and the desktop app share one form, since the desktop is that page in a
|
|
90
|
+
window. The CLI keeps its token beside the daemon's state, mode 600, so signing
|
|
91
|
+
in there and in the desktop app are the same thing on disk. The password is read
|
|
92
|
+
with the echo off and is never written down.
|
|
93
|
+
|
|
94
|
+
No magic link. A link in an inbox is no use on a television, or on a phone that
|
|
95
|
+
is not the one you read mail on.
|
|
96
|
+
|
|
97
|
+
Running the account side of nixamp.com needs Postgres:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
DATABASE_URL=postgres://user:pass@host/nixamp NIXAMP_JWT_SECRET=… nixamp serve --directory
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Accounts live where the directory lives and nowhere else: a nixamp on a laptop
|
|
104
|
+
has nobody to be an account of.
|
|
105
|
+
|
|
106
|
+
## The directory
|
|
107
|
+
|
|
108
|
+
[nixamp.com/directory](https://nixamp.com/directory) lists nixamps that agreed
|
|
109
|
+
to be listed. In the PWA, **Browse the directory** next to the address field
|
|
110
|
+
picks one without typing anything.
|
|
111
|
+
|
|
112
|
+
`nixamp serve` asks before listing you, and shows the exact link it would
|
|
113
|
+
publish:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
List this stream at https://nixamp.com/directory so anyone can find it?
|
|
117
|
+
It publishes http://198.51.100.7:4321/s/Lk1EM_mP977e1VT — listen only,
|
|
118
|
+
not the controls. [Y/n]
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Yes is the default; `--publish` and `--no-publish` skip the question, `--name`
|
|
122
|
+
sets what it is called. A terminal that cannot ask never publishes, because
|
|
123
|
+
nobody being there to answer is not consent.
|
|
124
|
+
|
|
125
|
+
What gets published is a **listen-only** link. Every server mints two keys: the
|
|
126
|
+
one in your own share link drives the player, and the listen key can hear it
|
|
127
|
+
and nothing else. `/api/command` and `/api/source` answer 403 to a listen key,
|
|
128
|
+
so a stranger in the directory cannot pause your music or point your machine at
|
|
129
|
+
something else.
|
|
130
|
+
|
|
131
|
+
Entries expire a few minutes after a stream stops renewing, so the list is
|
|
132
|
+
always what is actually live.
|
|
133
|
+
|
|
134
|
+
## Several streams at once
|
|
135
|
+
|
|
136
|
+
A channel is one publisher and everybody listening to them. Two or three devices
|
|
137
|
+
can be live at the same time -- a phone, a desktop, a second window -- each with
|
|
138
|
+
its own audience.
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
GET /api/channels what is live now
|
|
142
|
+
POST /api/channels/<id> publish to one
|
|
143
|
+
GET /api/channels/<id> listen to one
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
One ffmpeg decodes each publisher once and the result is written to every
|
|
147
|
+
listener on that channel. A decode per listener would cost a core each and, for
|
|
148
|
+
a live stream, would not even agree with itself about what "now" is.
|
|
149
|
+
|
|
150
|
+
A listener who joins halfway through gets the stream from that moment, which is
|
|
151
|
+
what live means. Two publishers on **one** channel is refused; on two channels it
|
|
152
|
+
is the whole point.
|
|
153
|
+
|
|
154
|
+
Publishing is administering the server, so it needs the control link or the
|
|
155
|
+
owner's account. Listening only needs the share link, like any other audio.
|
|
156
|
+
|
|
157
|
+
## Streaming into it
|
|
158
|
+
|
|
159
|
+
A nixamp can be the thing you broadcast *to*, not just from.
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
nixamp serve ~/Music --rtmp-in 1935
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Then point OBS, Larix, or another ffmpeg at the URL it prints. RTMP is what
|
|
166
|
+
every native broadcaster already speaks, so there is no nixamp-shaped client to
|
|
167
|
+
install. ffmpeg does the listening, so this costs no extra dependency.
|
|
168
|
+
|
|
169
|
+
A browser cannot speak RTMP at all, so the web app uses HTTP instead: one long
|
|
170
|
+
`POST /api/ingest` where the platform allows a streaming request body, and
|
|
171
|
+
`POST /api/ingest/chunk` where it does not. All three end up in the same place.
|
|
172
|
+
|
|
173
|
+
One publisher at a time. A second is refused rather than mixed.
|
|
174
|
+
|
|
175
|
+
## Broadcasting out
|
|
176
|
+
|
|
177
|
+
Out to as many places as you like, at once:
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
nixamp serve ~/Music --rtmp youtube=<key> --rtmp x=<key> --rtmp tiktok=<key>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`youtube`, `x`, `facebook`, `tiktok`, `twitch` and `kick` are known by name and
|
|
184
|
+
need only a key; anything else takes a full `rtmp://host/app/key`.
|
|
185
|
+
|
|
186
|
+
One ffmpeg, one encode, many outputs, through the `tee` muxer. An ffmpeg per
|
|
187
|
+
destination is the obvious shape and it encodes the same frames four times.
|
|
188
|
+
Every output carries `onfail=ignore`, so one destination with an expired key
|
|
189
|
+
cannot take the others down with it.
|
|
190
|
+
|
|
191
|
+
The encoder settings come from PairUX, which learned them against the real
|
|
192
|
+
platforms: a one-second keyframe interval because YouTube stalls on ffmpeg's
|
|
193
|
+
default, a forced constant frame rate because a variable-rate source makes
|
|
194
|
+
YouTube report a stream falling behind, and `yuv420p` because that is what RTMP
|
|
195
|
+
platforms accept. Music has no picture, so a flat colour is generated: RTMP
|
|
196
|
+
wants a video track either way.
|
|
197
|
+
|
|
198
|
+
Stream keys are read from the command line or the environment and never from a
|
|
199
|
+
request. `/api/broadcast/destinations` shows names and URLs with the keys
|
|
200
|
+
redacted.
|
|
201
|
+
|
|
202
|
+
## Paying to listen
|
|
203
|
+
|
|
204
|
+
A stream serving a handful of friends costs nothing and asks nothing. Past five
|
|
205
|
+
people listening at once it is bandwidth somebody is paying for, so the gate
|
|
206
|
+
opens: the sixth listener gets a 402 with an
|
|
207
|
+
[x402](https://github.com/profullstack/x402-gateway) offer, and a dollar buys a
|
|
208
|
+
day.
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
NIXAMP_PAY_TO=0xYourAddress COINPAY_X402_KEY=cp_live_… nixamp serve ~/Music --x402
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Three things are deliberate. The count is of *live* listeners, so a stream
|
|
215
|
+
quietens back to free on its own. Only the audio is gated: a 402 on `/api/state`
|
|
216
|
+
would break the page that has to render the offer. And nobody is cut off
|
|
217
|
+
mid-track, because the gate is asked once, when a request arrives.
|
|
218
|
+
|
|
219
|
+
`NIXAMP_PRICE_CENTS` and `NIXAMP_PASS_MINUTES` change the terms; the defaults are
|
|
220
|
+
100 and 1440, which is the dollar and the day. A server that has agreed to be in
|
|
221
|
+
the directory can also be switched on and off from nixamp.com: the configuration
|
|
222
|
+
rides back on the heartbeat it is already sending.
|
|
223
|
+
|
|
224
|
+
## Leaving it running
|
|
225
|
+
|
|
226
|
+
`nixamp serve` holds a terminal. `nixamp daemon` does not.
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
nixamp daemon start ~/Music --open-port
|
|
230
|
+
nixamp daemon status
|
|
231
|
+
nixamp daemon stop
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Start writes down where it went and the key it minted, waits until the server
|
|
235
|
+
is actually answering before saying it started, and prints the share link. It is
|
|
236
|
+
one daemon per user, and the state lives in `$XDG_STATE_HOME/nixamp`.
|
|
237
|
+
|
|
238
|
+
## Who may administer a server
|
|
239
|
+
|
|
240
|
+
Two ways to be allowed, and they answer different questions.
|
|
241
|
+
|
|
242
|
+
**You hold its control link.** That is possession: you are at the machine, or
|
|
243
|
+
somebody at it sent you the link. It works with no account and no network.
|
|
244
|
+
|
|
245
|
+
**You own it.** `nixamp login` and then `nixamp serve` claims the server for the
|
|
246
|
+
account signed in on that machine, and from then on that account can administer
|
|
247
|
+
it from a phone anywhere, by signing in to nixamp.com in the browser.
|
|
248
|
+
|
|
249
|
+
The server cannot check a nixamp.com token itself, and should not: it holds no
|
|
250
|
+
part of that secret. It asks nixamp.com who the token belongs to and compares
|
|
251
|
+
the answer to the owner it recorded at startup. Delegating identity while
|
|
252
|
+
keeping authorisation local is what lets a nixamp on a laptop trust an account
|
|
253
|
+
it has never seen.
|
|
254
|
+
|
|
255
|
+
Answers are remembered for a minute, so admin requests do not each cost a round
|
|
256
|
+
trip, and a revoked session stops working in about a minute rather than at the
|
|
257
|
+
next restart. If nixamp.com cannot be reached, nobody becomes the owner — the
|
|
258
|
+
control link is the way in until it can.
|
|
259
|
+
|
|
260
|
+
Listening is never affected: `/api/state`, `/api/stream` and the page itself
|
|
261
|
+
stay open to whoever has the share link.
|
|
262
|
+
|
|
263
|
+
## Watching it
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
nixamp admin
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Who is connected, from where, to what, for how long and how much has gone out.
|
|
270
|
+
It reads the daemon's own state file, so it needs no arguments; point it
|
|
271
|
+
anywhere else with `--url` and `--key`.
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
╭─ Server ─────────────────────────╮ ╭─ Now playing ────────────────────╮
|
|
275
|
+
│ http://127.0.0.1:4321 │ │ long.flac │
|
|
276
|
+
│ /home/anthony/Music │ │ — │
|
|
277
|
+
│ Uptime 3s │ │ State stopped │
|
|
278
|
+
│ Tracks 1 │ │ Position 0s │
|
|
279
|
+
│ Listeners 2 │ │ │
|
|
280
|
+
╰──────────────────────────────────╯ ╰──────────────────────────────────╯
|
|
281
|
+
╭─ Connections (2 live) ────────────────────────────────────────────────╮
|
|
282
|
+
│ Where Network Kind Client Track For Sent │
|
|
283
|
+
│ 10.0.0.42 private media VLC 3 long.flac 3s 2.6 MiB │
|
|
284
|
+
│ 100.65.1.7 tailscale stream Safari 17 long.flac 41s 18 MiB │
|
|
285
|
+
╰───────────────────────────────────────────────────────────────────────╯
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Press `r` to re-stream: hand the running server a different URL or path and the
|
|
289
|
+
listeners stay connected while what they are hearing changes under them.
|
|
290
|
+
|
|
66
291
|
## How it works
|
|
67
292
|
|
|
68
293
|
One decode feeds both your speakers and the display. `ffmpeg` writes raw 32-bit float samples to a pipe; nixamp reads every sample on its way past, runs an FFT over it, and hands the same bytes to `ffplay`.
|
package/bin/nixamp.mjs
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
import { main } from "../dist/main.js";
|
|
3
3
|
|
|
4
4
|
main().catch((error) => {
|
|
5
|
-
|
|
5
|
+
// A message we wrote is a message the user can act on; anything else is a
|
|
6
|
+
// bug and deserves its stack.
|
|
7
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8
|
+
console.error(message.startsWith("nixamp:") ? message : error);
|
|
6
9
|
process.exit(1);
|
|
7
10
|
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface Account {
|
|
2
|
+
id: string;
|
|
3
|
+
email: string;
|
|
4
|
+
}
|
|
5
|
+
export interface AuthResult {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
account: Account | null;
|
|
8
|
+
token: string;
|
|
9
|
+
/** Safe to show a stranger: it never says whether an address is registered. */
|
|
10
|
+
error: string;
|
|
11
|
+
}
|
|
12
|
+
export interface AccountsOptions {
|
|
13
|
+
/** postgres://user:pass@host/db */
|
|
14
|
+
connectionString: string;
|
|
15
|
+
/** Signing secret. Without one, every session dies on restart. */
|
|
16
|
+
secret: string;
|
|
17
|
+
/** Injected by the tests, which have no database. */
|
|
18
|
+
system?: AuthLike;
|
|
19
|
+
}
|
|
20
|
+
/** The slice of the auth system nixamp uses. */
|
|
21
|
+
export interface AuthLike {
|
|
22
|
+
register(input: {
|
|
23
|
+
email: string;
|
|
24
|
+
password: string;
|
|
25
|
+
autoVerify?: boolean;
|
|
26
|
+
}): Promise<unknown>;
|
|
27
|
+
login(input: {
|
|
28
|
+
email: string;
|
|
29
|
+
password: string;
|
|
30
|
+
}): Promise<unknown>;
|
|
31
|
+
validateToken(token: string): Promise<unknown>;
|
|
32
|
+
}
|
|
33
|
+
/** Pull an account and a token out of whatever shape the module returned. */
|
|
34
|
+
export declare function readResult(value: unknown): AuthResult;
|
|
35
|
+
/** `validateToken` answers claims directly, unlike login and register. */
|
|
36
|
+
export declare function readClaims(value: unknown): Account | null;
|
|
37
|
+
/** An address that could exist, and a password long enough to be worth having. */
|
|
38
|
+
export declare function checkCredentials(email: unknown, password: unknown): string;
|
|
39
|
+
export declare class Accounts {
|
|
40
|
+
private readonly system;
|
|
41
|
+
constructor(options: AccountsOptions);
|
|
42
|
+
signUp(email: unknown, password: unknown): Promise<AuthResult>;
|
|
43
|
+
signIn(email: unknown, password: unknown): Promise<AuthResult>;
|
|
44
|
+
whoIs(token: string): Promise<Account | null>;
|
|
45
|
+
}
|
|
46
|
+
/** The bearer token on a request, from the header or the session cookie. */
|
|
47
|
+
export declare function tokenFrom(headers: Record<string, string | string[] | undefined>): string;
|
|
48
|
+
/**
|
|
49
|
+
* The session cookie. HttpOnly because nothing in the page reads it -- the
|
|
50
|
+
* browser attaches it by itself -- and Secure only where the page was served
|
|
51
|
+
* over https, since a nixamp on your own network is plain http.
|
|
52
|
+
*/
|
|
53
|
+
export declare function sessionCookie(token: string, secure: boolean): string;
|
|
54
|
+
export declare function clearedCookie(): string;
|
package/dist/accounts.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accounts on nixamp.com.
|
|
3
|
+
*
|
|
4
|
+
* The house auth module does the work: password and JWT, over the Postgres
|
|
5
|
+
* adapter. This is the shape nixamp needs around it, and the two things the
|
|
6
|
+
* module gets wrong from a caller's point of view:
|
|
7
|
+
*
|
|
8
|
+
* - `login()` and `register()` THROW on a bad password or a taken address
|
|
9
|
+
* rather than resolving `{ success: false }`, so a bare `if (!result.success)`
|
|
10
|
+
* never runs. Everything here answers a result instead.
|
|
11
|
+
* - `validateToken()` resolves to the claims directly, not to a wrapper like
|
|
12
|
+
* the other two, so the shapes differ between calls.
|
|
13
|
+
* - `register()` without `autoVerify` creates an account that `login()` will
|
|
14
|
+
* refuse for ever, and returns no tokens. nixamp sends no email, so there
|
|
15
|
+
* would be nothing to click.
|
|
16
|
+
*
|
|
17
|
+
* No magic link: a link in an inbox is no use on a television or a phone that
|
|
18
|
+
* is not the one you read mail on.
|
|
19
|
+
*/
|
|
20
|
+
import { createAuthSystem, PostgresAdapter } from "@profullstack/auth-system";
|
|
21
|
+
const NO_ACCOUNT = { ok: false, account: null, token: "", error: "" };
|
|
22
|
+
/**
|
|
23
|
+
* The same sentence for a wrong password and an address with no account.
|
|
24
|
+
* Saying which is how an endpoint tells a stranger who has registered.
|
|
25
|
+
*/
|
|
26
|
+
const REFUSED = "that email and password do not match an account";
|
|
27
|
+
/** Pull an account and a token out of whatever shape the module returned. */
|
|
28
|
+
export function readResult(value) {
|
|
29
|
+
const record = (value ?? {});
|
|
30
|
+
const user = (record["user"] ?? {});
|
|
31
|
+
const tokens = (record["tokens"] ?? {});
|
|
32
|
+
const id = typeof user["id"] === "string" ? user["id"] : "";
|
|
33
|
+
const email = typeof user["email"] === "string" ? user["email"] : "";
|
|
34
|
+
const token = typeof tokens["accessToken"] === "string" ? tokens["accessToken"] : "";
|
|
35
|
+
if (!id || !token)
|
|
36
|
+
return { ...NO_ACCOUNT, error: REFUSED };
|
|
37
|
+
return { ok: true, account: { id, email }, token, error: "" };
|
|
38
|
+
}
|
|
39
|
+
/** `validateToken` answers claims directly, unlike login and register. */
|
|
40
|
+
export function readClaims(value) {
|
|
41
|
+
const claims = (value ?? {});
|
|
42
|
+
const id = typeof claims["userId"] === "string" ? claims["userId"] : "";
|
|
43
|
+
const email = typeof claims["email"] === "string" ? claims["email"] : "";
|
|
44
|
+
return id ? { id, email } : null;
|
|
45
|
+
}
|
|
46
|
+
/** An address that could exist, and a password long enough to be worth having. */
|
|
47
|
+
export function checkCredentials(email, password) {
|
|
48
|
+
if (typeof email !== "string" || !/^[^@\s]+@[^@\s.]+\.[^@\s]+$/.test(email)) {
|
|
49
|
+
return "that does not look like an email address";
|
|
50
|
+
}
|
|
51
|
+
if (typeof password !== "string" || password.length < 10) {
|
|
52
|
+
// Length is checked here so a hopeless password never reaches the
|
|
53
|
+
// database. The auth module then applies its own composition rules on top,
|
|
54
|
+
// and its refusals are passed through rather than swallowed.
|
|
55
|
+
return "a password needs at least 10 characters";
|
|
56
|
+
}
|
|
57
|
+
if (password.length > 200)
|
|
58
|
+
return "that password is too long";
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
export class Accounts {
|
|
62
|
+
system;
|
|
63
|
+
constructor(options) {
|
|
64
|
+
this.system =
|
|
65
|
+
options.system ??
|
|
66
|
+
createAuthSystem({
|
|
67
|
+
adapter: new PostgresAdapter({ connectionString: options.connectionString }),
|
|
68
|
+
jwtSecret: options.secret,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async signUp(email, password) {
|
|
72
|
+
const wrong = checkCredentials(email, password);
|
|
73
|
+
if (wrong)
|
|
74
|
+
return { ...NO_ACCOUNT, error: wrong };
|
|
75
|
+
try {
|
|
76
|
+
// autoVerify does two things, and both are necessary here: without it
|
|
77
|
+
// the account is created unverified and login() refuses it forever --
|
|
78
|
+
// nixamp sends no email, so there is nothing to click -- and register()
|
|
79
|
+
// returns no tokens, so signing up would not sign you in.
|
|
80
|
+
return readResult(await this.system.register({
|
|
81
|
+
email: email,
|
|
82
|
+
password: password,
|
|
83
|
+
autoVerify: true,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const message = error.message ?? "";
|
|
88
|
+
// "already exists" is the one case worth naming: a sign-up form that
|
|
89
|
+
// will not say why is a sign-up form people give up on. It reveals
|
|
90
|
+
// nothing that trying to sign up does not reveal anyway.
|
|
91
|
+
if (/exist|taken|duplicate/i.test(message)) {
|
|
92
|
+
return { ...NO_ACCOUNT, error: "there is already an account with that email" };
|
|
93
|
+
}
|
|
94
|
+
// The module has its own password rules -- an uppercase letter, and so
|
|
95
|
+
// on -- and refuses with a sentence saying which. Hiding that behind
|
|
96
|
+
// "could not create that account" leaves someone retyping a password
|
|
97
|
+
// that will never be accepted.
|
|
98
|
+
const complaint = /^Invalid (?:password|email)[:\s]+(.*)$/i.exec(message);
|
|
99
|
+
if (complaint?.[1])
|
|
100
|
+
return { ...NO_ACCOUNT, error: complaint[1].trim().toLowerCase() };
|
|
101
|
+
return { ...NO_ACCOUNT, error: "could not create that account" };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async signIn(email, password) {
|
|
105
|
+
if (checkCredentials(email, password))
|
|
106
|
+
return { ...NO_ACCOUNT, error: REFUSED };
|
|
107
|
+
try {
|
|
108
|
+
return readResult(await this.system.login({ email: email, password: password }));
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// login() throws on bad credentials, so this is the ordinary path.
|
|
112
|
+
return { ...NO_ACCOUNT, error: REFUSED };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async whoIs(token) {
|
|
116
|
+
if (!token)
|
|
117
|
+
return null;
|
|
118
|
+
try {
|
|
119
|
+
return readClaims(await this.system.validateToken(token));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** The bearer token on a request, from the header or the session cookie. */
|
|
127
|
+
export function tokenFrom(headers) {
|
|
128
|
+
const authorization = headers["authorization"];
|
|
129
|
+
const header = Array.isArray(authorization) ? authorization[0] : authorization;
|
|
130
|
+
const bearer = /^Bearer\s+(.+)$/i.exec(header ?? "")?.[1];
|
|
131
|
+
if (bearer)
|
|
132
|
+
return bearer.trim();
|
|
133
|
+
const cookie = Array.isArray(headers["cookie"]) ? headers["cookie"][0] : headers["cookie"];
|
|
134
|
+
for (const part of (cookie ?? "").split(";")) {
|
|
135
|
+
const [name, ...rest] = part.trim().split("=");
|
|
136
|
+
if (name === "nixamp_session" && rest.length > 0)
|
|
137
|
+
return decodeURIComponent(rest.join("="));
|
|
138
|
+
}
|
|
139
|
+
return "";
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The session cookie. HttpOnly because nothing in the page reads it -- the
|
|
143
|
+
* browser attaches it by itself -- and Secure only where the page was served
|
|
144
|
+
* over https, since a nixamp on your own network is plain http.
|
|
145
|
+
*/
|
|
146
|
+
export function sessionCookie(token, secure) {
|
|
147
|
+
const parts = [
|
|
148
|
+
`nixamp_session=${encodeURIComponent(token)}`,
|
|
149
|
+
"Path=/",
|
|
150
|
+
"Max-Age=2592000",
|
|
151
|
+
"SameSite=Lax",
|
|
152
|
+
"HttpOnly",
|
|
153
|
+
];
|
|
154
|
+
if (secure)
|
|
155
|
+
parts.push("Secure");
|
|
156
|
+
return parts.join("; ");
|
|
157
|
+
}
|
|
158
|
+
export function clearedCookie() {
|
|
159
|
+
return "nixamp_session=; Path=/; Max-Age=0; SameSite=Lax; HttpOnly";
|
|
160
|
+
}
|
package/dist/admin.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nixamp admin` — what the daemon is doing, and who is listening to it.
|
|
3
|
+
*
|
|
4
|
+
* It talks to a running server over the same HTTP API a browser uses, so it
|
|
5
|
+
* works against the local daemon, against `nixamp serve` in another terminal,
|
|
6
|
+
* or against a nixamp on a different machine entirely.
|
|
7
|
+
*/
|
|
8
|
+
import { type Container, type Theme } from "@profullstack/hqtui";
|
|
9
|
+
import type { Connection } from "./connections.ts";
|
|
10
|
+
interface Report {
|
|
11
|
+
connections: Connection[];
|
|
12
|
+
active: number;
|
|
13
|
+
startedAt: number;
|
|
14
|
+
now: number;
|
|
15
|
+
}
|
|
16
|
+
interface Snapshot {
|
|
17
|
+
tracks: {
|
|
18
|
+
title: string;
|
|
19
|
+
artist: string;
|
|
20
|
+
duration: number;
|
|
21
|
+
}[];
|
|
22
|
+
index: number;
|
|
23
|
+
playing: boolean;
|
|
24
|
+
position: number;
|
|
25
|
+
root: string;
|
|
26
|
+
note: string;
|
|
27
|
+
}
|
|
28
|
+
export interface AdminOptions {
|
|
29
|
+
url: string;
|
|
30
|
+
key: string | null;
|
|
31
|
+
}
|
|
32
|
+
/** Where to point, from the flags or from the daemon that is running. */
|
|
33
|
+
export declare function resolveTarget(argv: string[]): AdminOptions;
|
|
34
|
+
/** Seconds as something a person reads at a glance. */
|
|
35
|
+
export declare function since(ms: number): string;
|
|
36
|
+
export declare function bytes(value: number): string;
|
|
37
|
+
export declare function admin(argv: string[]): Promise<void>;
|
|
38
|
+
export interface View {
|
|
39
|
+
url: string;
|
|
40
|
+
report: Report | null;
|
|
41
|
+
snapshot: Snapshot | null;
|
|
42
|
+
error: string;
|
|
43
|
+
typing: boolean;
|
|
44
|
+
restreaming: string;
|
|
45
|
+
}
|
|
46
|
+
export declare function draw(ui: Container, theme: Theme, view: View): void;
|
|
47
|
+
export {};
|