@gjermundgaraba/clankerauth 0.24.0 → 0.25.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/main.mjs +22 -6
  3. package/package.json +11 -11
package/README.md CHANGED
@@ -12,7 +12,7 @@ clankerauth create-api-key --name "notes sync" \
12
12
  clankerauth logout
13
13
  ```
14
14
 
15
- `login <url>` signs in to the resource `url` identifies, reading it from its `/.well-known/oauth-protected-resource`: the issuer's own origin, `CLANKERAUTH_BASE_URL`, publishes administration, and an application's URL publishes its resource. It then discovers the issuer that resource names; registers the command there as a native OAuth client, a new one for each sign-in; prints the approval URL on stderr for the owner to open; and listens for the redirect on a loopback port. It asks for every scope the resource publishes, `clankerauth:read clankerauth:write` for administration, unless `--scope` names fewer: `--scope clankerauth:read` lists and never changes. Beside them it asks for `offline_access`, alone for a resource that publishes no scopes. A resource must publish that document, and so be identified by an `http:` or `https:` URL, to be signed in to.
15
+ `login <url>` signs in to the resource `url` identifies, reading it from its `/.well-known/oauth-protected-resource`: the issuer's own origin, `CLANKERAUTH_BASE_URL`, publishes administration, and an application's URL publishes its resource. It then discovers the issuer that resource names; registers the command there as a native OAuth client, a new one for each sign-in; opens the approval page in the browser and prints its URL on stderr, for a terminal with no browser to open, or alone with `--no-browser`; and listens for the redirect on a loopback port. It asks for every scope the resource publishes, `clankerauth:read clankerauth:write` for administration, unless `--scope` names fewer: `--scope clankerauth:read` lists and never changes. Beside them it asks for `offline_access`, alone for a resource that publishes no scopes. A resource must publish that document, and so be identified by an `http:` or `https:` URL, to be signed in to.
16
16
 
17
17
  The sign-ins are kept in `$XDG_CONFIG_HOME/clankerauth/sign-ins.json`, `~/.config/clankerauth/sign-ins.json` by default, readable by their owner alone: one per resource, each with its own issuer, so signing in at a second issuer leaves the first's alone. A file the command cannot read, such as one another version wrote, counts as signed out and the next `login` replaces it. A `login` to a resource already signed in to replaces that sign-in and revokes it. A command refreshes an access token as it is about to expire and saves the rotated refresh token, so a sign-in lasts while it is used at least every 30 days; when the issuer refuses a refresh, the error names the `login` that asks for the same scopes again. Every change to the file holds its lock, so commands and programs running at once refresh an expired sign-in once, and `logout` revokes the refresh token that is current. `logout` revokes every sign-in and deletes the file, and `logout <url>` the one whose resource that URL is under alone.
18
18
 
package/dist/main.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
2
3
  import { Config, Console, Context, Effect, Option, Redacted, Schema } from "effect";
3
4
  import { Argument, CliError, Command, Flag } from "effect/cli";
4
5
  import { FetchHttpClient, HttpClient } from "effect/http";
@@ -399,7 +400,7 @@ const OwnerHttp = ActionHttp.make(Administration, {
399
400
  });
400
401
  //#endregion
401
402
  //#region package.json
402
- var version = "0.24.0";
403
+ var version = "0.25.0";
403
404
  //#endregion
404
405
  //#region src/main.ts
405
406
  const refused = (error) => new CliError.UserError({
@@ -408,14 +409,29 @@ const refused = (error) => new CliError.UserError({
408
409
  });
409
410
  const json = (value) => Console.log(JSON.stringify(value, null, 2));
410
411
  const urlArgument = (description) => Argument.String("url").pipe(Argument.withDescription(description), Argument.optional);
411
- const approve = (page) => Effect.sync(() => process.stderr.write(`Open this URL in your browser to approve the sign-in:\n\n ${page.href}\n\n`));
412
+ const browserOpener = (url) => {
413
+ switch (process.platform) {
414
+ case "darwin": return ["open", [url]];
415
+ case "win32": return ["rundll32", ["url.dll,FileProtocolHandler", url]];
416
+ default: return ["xdg-open", [url]];
417
+ }
418
+ };
419
+ const openInBrowser = (page) => Effect.sync(() => {
420
+ const [command, args] = browserOpener(page.href);
421
+ spawn(command, args, {
422
+ stdio: "ignore",
423
+ detached: true
424
+ }).on("error", () => void 0).unref();
425
+ });
426
+ const approve = (browser) => (page) => browser ? Effect.andThen(Effect.sync(() => process.stderr.write(`Approve the sign-in in your browser. If it did not open, open this URL:\n\n ${page.href}\n\n`)), openInBrowser(page)) : Effect.sync(() => process.stderr.write(`Open this URL in your browser to approve the sign-in:\n\n ${page.href}\n\n`));
412
427
  const loginCommand = Command.make("login", {
413
428
  url: Argument.String("url").pipe(Argument.withDescription("The resource to sign in to: an application's URL, such as https://notes.example, or the issuer's origin, its CLANKERAUTH_BASE_URL, for administration")),
414
- scope: Flag.String("scope").pipe(Flag.withDescription("The scopes to ask for, space-separated: every scope the resource publishes by default"), Flag.optional)
415
- }, ({ url, scope }) => Credentials.signIn(url, {
429
+ scope: Flag.String("scope").pipe(Flag.withDescription("The scopes to ask for, space-separated: every scope the resource publishes by default"), Flag.optional),
430
+ noBrowser: Flag.Boolean("no-browser").pipe(Flag.withDescription("Print the approval URL without opening it in the browser"), Flag.withDefault(false))
431
+ }, ({ url, scope, noBrowser }) => Credentials.signIn(url, {
416
432
  scope: Option.getOrUndefined(scope),
417
- approve
418
- }).pipe(Effect.mapError(refused), Effect.flatMap(json))).pipe(Command.withDescription("Sign in as the issuer's owner to a resource: approve the URL it prints in the browser. The resource's issuer and scopes are read from its /.well-known/oauth-protected-resource. Sign-ins are kept in ~/.config/clankerauth, each beside the others, whatever its issuer."));
433
+ approve: approve(!noBrowser)
434
+ }).pipe(Effect.mapError(refused), Effect.flatMap(json))).pipe(Command.withDescription("Sign in as the issuer's owner to a resource: approve the page it opens in the browser, or the URL it prints. The resource's issuer and scopes are read from its /.well-known/oauth-protected-resource. Sign-ins are kept in ~/.config/clankerauth, each beside the others, whatever its issuer."));
419
435
  const logoutCommand = Command.make("logout", { url: urlArgument("Sign out of the resource this URL is under alone") }, ({ url }) => Credentials.signOut(Option.getOrUndefined(url)).pipe(Effect.mapError(refused), Effect.flatMap(json))).pipe(Command.withDescription("Revoke the sign-ins, or the one to the resource a URL is under, and forget them."));
420
436
  const notSignedIn = new CliError.UserError({
421
437
  cause: "not signed in",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjermundgaraba/clankerauth",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "The clankerauth command line: sign in to an issuer as its owner and administer it over HTTP.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -19,19 +19,19 @@
19
19
  "access": "public"
20
20
  },
21
21
  "dependencies": {
22
- "@effect/platform-node": "4.0.4",
23
- "@effect/platform-node-shared": "4.0.4",
24
- "@gjermundgaraba/clankerauth-sdk": "0.24.0",
25
- "@gjermundgaraba/effect-actions": "0.18.0",
26
- "effect": "4.0.4"
22
+ "@effect/platform-node": "4.0.5",
23
+ "@effect/platform-node-shared": "4.0.5",
24
+ "@gjermundgaraba/clankerauth-sdk": "0.25.0",
25
+ "@gjermundgaraba/effect-actions": "0.18.2",
26
+ "effect": "4.0.5"
27
27
  },
28
28
  "devDependencies": {
29
- "@gjermundgaraba/clankerauth-admin-api": "0.24.0",
30
- "@gjermundgaraba/clankerauth-dev": "0.24.0",
31
- "@types/node": "26.6.4",
29
+ "@gjermundgaraba/clankerauth-admin-api": "0.25.0",
30
+ "@gjermundgaraba/clankerauth-dev": "0.25.0",
31
+ "@types/node": "26.6.5",
32
32
  "typescript": "7.0.2",
33
- "vite": "npm:@voidzero-dev/vite-plus-core@1.2.0",
34
- "vite-plus": "1.2.0"
33
+ "vite": "npm:@voidzero-dev/vite-plus-core@1.3.0",
34
+ "vite-plus": "1.3.0"
35
35
  },
36
36
  "engines": {
37
37
  "node": ">=26"