@gjsify/system-accounts 0.0.0-stage → 0.55.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 +64 -2
- package/lib/esm/_virtual/_rolldown/runtime.js +1 -0
- package/lib/esm/goa.js +1 -0
- package/lib/esm/index.js +1 -0
- package/lib/esm/mapping.js +1 -0
- package/lib/esm/no-account-store.js +1 -0
- package/lib/esm/system-accounts.js +1 -0
- package/lib/esm/types.js +0 -0
- package/lib/types/fake-goa.d.ts +43 -0
- package/lib/types/goa.d.ts +9 -0
- package/lib/types/goa.gjs.spec.d.ts +2 -0
- package/lib/types/system-accounts.d.ts +3 -0
- package/package.json +88 -5
package/README.md
CHANGED
|
@@ -1,3 +1,65 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @gjsify/system-accounts
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The accounts a user already set up on their system: find them, read their mail server settings, log in without asking for a password again. The interface is the capabilities a caller needs; a driver per platform maps the system's account store onto them ([ADR 0095](../../../docs/adr/0095-system-accounts-are-asked-by-capability.md)).
|
|
4
|
+
|
|
5
|
+
Part of the [gjsify](https://github.com/gjsify/gjsify) project. **Tier 3 (experimental, ADR 0003)**: new, one driver, no promise yet.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
gjsify install @gjsify/system-accounts
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { createSystemAccounts, isUnavailable } from '@gjsify/system-accounts';
|
|
17
|
+
|
|
18
|
+
const accounts = createSystemAccounts();
|
|
19
|
+
if (accounts.capabilities().services.length === 0) {
|
|
20
|
+
// No account store on this host: ask the user instead.
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const account of await accounts.listAccounts('mail')) {
|
|
24
|
+
const mail = await accounts.getMailSettings(account);
|
|
25
|
+
if (isUnavailable(mail) || !mail.smtp) continue;
|
|
26
|
+
|
|
27
|
+
const credentials = await accounts.getCredentials(account, 'smtp');
|
|
28
|
+
if (credentials.kind === 'password') login(mail.smtp, credentials.secret);
|
|
29
|
+
if (credentials.kind === 'oauth2') loginXoauth2(mail.smtp, credentials.token); // valid until credentials.expiresAt
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
| Call | Answers |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `capabilities()` | what THIS host can answer: `services` it lists, `credentials` kinds it hands out. Both empty without an account store |
|
|
36
|
+
| `listAccounts(service?)` | `Account[]` (id, provider, identity, label, enabled services). Empty when the store cannot be reached |
|
|
37
|
+
| `getMailSettings(account)` | `{ sender, imap?, smtp? }`, each server as host, port, `security` (`tls` / `starttls` / `none`) and user name; or `Unavailable` |
|
|
38
|
+
| `getCredentials(account, purpose)` | `{ kind: 'password', secret }`, `{ kind: 'oauth2', token, expiresAt }`, or `Unavailable` |
|
|
39
|
+
|
|
40
|
+
**A missing capability is a value, never an exception.** `Unavailable` carries a `reason`: `no-account-store`, `unknown-account`, `not-supported` (the account has no mail server, no credentials) or `credentials-unavailable` (the store would not hand the secret out). The ADR's `Credentials` has two members; the third is how that rule is spelled for a call that has to return one.
|
|
41
|
+
|
|
42
|
+
**Secrets** go to the caller and nowhere else: never logged, cached or put in an error. They are read per call; an OAuth2 token carries its expiry, so ask again afterwards instead of keeping it.
|
|
43
|
+
|
|
44
|
+
## Platforms
|
|
45
|
+
|
|
46
|
+
| Platform | State |
|
|
47
|
+
|---|---|
|
|
48
|
+
| Linux, GNOME | **driver**: GNOME Online Accounts through `libgoa` (`gi://Goa?version=1.0&optional`). A host without the typelib still starts and reports empty capabilities |
|
|
49
|
+
| Linux without GOA, macOS, Windows | no driver; empty capabilities. Whether a platform offers a usable account store to third-party apps is **unexplored**, not ruled out |
|
|
50
|
+
| Android, iOS, browser | not served (`gjsify.runtimes`: only `gjs` is `native`) |
|
|
51
|
+
|
|
52
|
+
IMAP and SMTP passwords are looked up under the ids GOA keeps them under (`imap-password`, `smtp-password`); SMTP falls back to the stored IMAP password, because one login often serves both.
|
|
53
|
+
|
|
54
|
+
## Testing
|
|
55
|
+
|
|
56
|
+
`libgoa` has no constructor that takes a connection: `Goa.Client.new` dials the session bus. `src/fake-goa.ts` therefore exports the interfaces it calls (`ObjectManager`, `PasswordBased`, `OAuth2Based`, and the few bus methods a client needs) on a peer-to-peer `Gio.DBusServer` and points the session address at it, so the driver runs libgoa's own client code with no GNOME session, no real account and no keyring. Dummy secrets are compared, never printed.
|
|
57
|
+
|
|
58
|
+
- GLib caches the session connection per process. Run the suite through `test:gjs`, which starts it with a dead bus path; the fake refuses to go on if the process is already bound to a real bus, so a developer's accounts never reach a test.
|
|
59
|
+
- The mapping from account fields to the contract (host/port splitting, `tls`/`starttls`/`none` from the two flags and the port, which password ids to try) is pure and runs on Node too.
|
|
60
|
+
- Not covered: a real session bus, a real provider (the OAuth2 path has run against the fake only), and a host whose `libgoa` is genuinely missing (the empty-capabilities path is tested by passing no namespace).
|
|
61
|
+
- The `&optional` import costs a top-level await in the bundle that reaches it, and a `@gjsify/unit` run wedges on it (ADR 0085 clause 5). The specs therefore reach the driver (`src/goa.ts`) directly; only `createSystemAccounts` (`src/system-accounts.ts`) carries the flag.
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=Object.defineProperty,__name=(t,n)=>e(t,`name`,{value:n,configurable:!0});export{__name};
|
package/lib/esm/goa.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{enabledServices as e,mailSettingsFrom as t,passwordIds as n}from"./mapping.js";import{isUnavailable as r,noAccountStore as i}from"./no-account-store.js";import a from"gi://GLib?version=2.0";import o from"gi://Gio?version=2.0";const unavailable=e=>({kind:`unavailable`,reason:e});function newClient(e,t){return new Promise((n,r)=>{e.Client.new(t,(t,i)=>{try{n(e.Client.new_finish(i))}catch(e){r(e)}})})}async function connect(e,t){let n=new o.Cancellable,r=a.timeout_add(a.PRIORITY_DEFAULT,t,()=>(n.cancel(),a.SOURCE_REMOVE));try{return await newClient(e,n)}catch{return}finally{a.Source.remove(r)}}function secretOf(e,t){return new Promise(n=>{e.call_get_password(t,null,(t,r)=>{try{let[t,i]=e.call_get_password_finish(r);n(t&&i?i:void 0)}catch{n(void 0)}})})}function tokenOf(e){return new Promise(t=>{e.call_get_access_token(null,(n,r)=>{try{let[n,i,a]=e.call_get_access_token_finish(r);t(n&&i?[i,a]:void 0)}catch{t(void 0)}})})}function toAccount(t){let n=t.get_account();if(n)return{id:n.id??``,provider:n.provider_type??``,providerName:n.provider_name??``,identity:n.identity??``,label:n.presentation_identity??``,services:e({mail:!!t.get_mail(),calendar:!!t.get_calendar(),contacts:!!t.get_contacts(),files:!!t.get_files()},{mail:!!n.mail_disabled,calendar:!!n.calendar_disabled,contacts:!!n.contacts_disabled,files:!!n.files_disabled})}}function createGoaAccounts({goa:e,now:a=Date.now,connectTimeoutMs:o=5e3}){if(!e)return i;let s,client=async()=>{s??=connect(e,o);let t=await s;return t||(s=void 0),t};async function find(e){let t=await client();return t?t.lookup_by_id(e.id)??unavailable(`unknown-account`):unavailable(`no-account-store`)}return{capabilities:()=>({services:[`mail`,`calendar`,`contacts`,`files`],credentials:[`password`,`oauth2`]}),async listAccounts(e){let t=await client();if(!t)return[];let n=t.get_accounts().flatMap(e=>toAccount(e)??[]);return e?n.filter(t=>t.services.includes(e)):n},async getMailSettings(e){let n=await find(e);if(r(n))return n;let i=n.get_mail();return i?t({emailAddress:i.email_address??``,name:i.name??``,imapSupported:i.imap_supported,imap:{host:i.imap_host??``,userName:i.imap_user_name??``,useSsl:i.imap_use_ssl,useTls:i.imap_use_tls},smtpSupported:i.smtp_supported,smtp:{host:i.smtp_host??``,userName:i.smtp_user_name??``,useSsl:i.smtp_use_ssl,useTls:i.smtp_use_tls,authenticate:i.smtp_use_auth}})??unavailable(`not-supported`):unavailable(`not-supported`)},async getCredentials(e,t){let i=await find(e);if(r(i))return i;let o=i.get_oauth2_based();if(o){let e=await tokenOf(o);if(!e)return unavailable(`credentials-unavailable`);let[t,n]=e;return{kind:`oauth2`,token:t,expiresAt:a()+n*1e3}}let s=i.get_password_based();if(!s)return unavailable(`not-supported`);for(let e of n(t)){let t=await secretOf(s,e);if(t!==void 0)return{kind:`password`,secret:t}}return unavailable(`credentials-unavailable`)}}}export{createGoaAccounts};
|
package/lib/esm/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{isUnavailable as e}from"./no-account-store.js";import{createSystemAccounts as t}from"./system-accounts.js";export{t as createSystemAccounts,e as isUnavailable};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";function splitHostPort(e){let t=e.trim(),n=/^\[([^\]]+)\](?::(\d*))?$/.exec(t);if(n)return{host:n[1],port:parsePort(n[2])};let r=t.indexOf(`:`);return r===-1||r!==t.lastIndexOf(`:`)?{host:t}:{host:t.slice(0,r),port:parsePort(t.slice(r+1))}}function parsePort(e){if(!e||!/^\d+$/.test(e))return;let t=Number.parseInt(e,10);return t>=1&&t<=65535?t:void 0}const e={imap:993,smtp:465},t={imap:{tls:993,starttls:143,none:143},smtp:{tls:465,starttls:587,none:25}};function securityFromFlags(t,n,r,i){return n?`tls`:r?`starttls`:i===e[t]?`tls`:`none`}function serverSettingsFrom(e,n){let{host:r,port:i}=splitHostPort(n.host);if(!r)return;let a=securityFromFlags(e,n.useSsl,n.useTls,i);return{host:r,port:i??t[e][a],security:a,...n.authenticate&&n.userName?{username:n.userName}:{}}}function mailSettingsFrom(e){let t=e.imapSupported?serverSettingsFrom(`imap`,{...e.imap,authenticate:!0}):void 0,n=e.smtpSupported?serverSettingsFrom(`smtp`,e.smtp):void 0;if(!(!t&&!n))return{sender:{address:e.emailAddress,...e.name?{name:e.name}:{}},...t?{imap:t}:{},...n?{smtp:n}:{}}}function enabledServices(e,t){return[`mail`,`calendar`,`contacts`,`files`].filter(n=>e[n]&&!t[n])}function passwordIds(e){return e===`imap`?[`imap-password`,`password`,``]:[`smtp-password`,`imap-password`,`password`,``]}export{enabledServices,mailSettingsFrom,passwordIds,securityFromFlags,serverSettingsFrom,splitHostPort};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";const e={kind:`unavailable`,reason:`no-account-store`},t={capabilities:()=>({services:[],credentials:[]}),listAccounts:async()=>[],getMailSettings:async()=>e,getCredentials:async()=>e};function isUnavailable(e){return`kind`in e&&e.kind===`unavailable`}export{isUnavailable,t as noAccountStore};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{createGoaAccounts as e}from"./goa.js";import t from"gi://Goa?version=1.0&optional";function createSystemAccounts(){return e({goa:t})}export{createSystemAccounts};
|
package/lib/esm/types.js
ADDED
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { AccountService } from './types.js';
|
|
2
|
+
export interface FakeMail {
|
|
3
|
+
readonly email: string;
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly imap?: {
|
|
6
|
+
host: string;
|
|
7
|
+
user: string;
|
|
8
|
+
ssl: boolean;
|
|
9
|
+
tls: boolean;
|
|
10
|
+
};
|
|
11
|
+
readonly smtp?: {
|
|
12
|
+
host: string;
|
|
13
|
+
user: string;
|
|
14
|
+
ssl: boolean;
|
|
15
|
+
tls: boolean;
|
|
16
|
+
auth: boolean;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export interface FakeAccount {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly provider?: string;
|
|
22
|
+
readonly identity?: string;
|
|
23
|
+
readonly disabled?: Partial<Record<AccountService, boolean>>;
|
|
24
|
+
readonly mail?: FakeMail;
|
|
25
|
+
readonly calendar?: boolean;
|
|
26
|
+
readonly contacts?: boolean;
|
|
27
|
+
readonly files?: boolean;
|
|
28
|
+
/** Secret id → dummy secret. */
|
|
29
|
+
readonly passwords?: Readonly<Record<string, string>>;
|
|
30
|
+
readonly oauth2?: {
|
|
31
|
+
readonly token: string;
|
|
32
|
+
readonly expiresIn: number;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export interface FakeGoa {
|
|
36
|
+
setAccounts(accounts: readonly FakeAccount[]): void;
|
|
37
|
+
/** The secret ids libgoa was asked for since `setAccounts`, never the secrets. */
|
|
38
|
+
readonly requestedPasswordIds: string[];
|
|
39
|
+
/** How many `GetManagedObjects` calls arrived: proof the driver talked to THIS server. */
|
|
40
|
+
managedObjectsCalls(): number;
|
|
41
|
+
}
|
|
42
|
+
/** One fake per process; later calls return the same one, so a test sets its own accounts. */
|
|
43
|
+
export declare function startFakeGoa(): Promise<FakeGoa>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type Goa from 'gi://Goa?version=1.0';
|
|
2
|
+
import type { SystemAccounts } from './types.js';
|
|
3
|
+
export interface GoaOptions {
|
|
4
|
+
/** The libgoa namespace, or `undefined` on a host without it. */
|
|
5
|
+
readonly goa: typeof Goa | undefined;
|
|
6
|
+
readonly now?: () => number;
|
|
7
|
+
readonly connectTimeoutMs?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function createGoaAccounts({ goa, now, connectTimeoutMs, }: GoaOptions): SystemAccounts;
|
package/package.json
CHANGED
|
@@ -1,6 +1,89 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@gjsify/system-accounts",
|
|
3
|
+
"version": "0.55.0",
|
|
4
|
+
"description": "The accounts a user already set up on their system: find them, read their mail server settings, get credentials. GNOME Online Accounts on Linux.",
|
|
5
|
+
"module": "lib/esm/index.js",
|
|
6
|
+
"types": "lib/types/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./lib/types/index.d.ts",
|
|
12
|
+
"default": "./lib/esm/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"lib",
|
|
17
|
+
"!lib/types/fake-goa.d.ts",
|
|
18
|
+
"!lib/types/**/*.spec.d.ts"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"clear": "gjsify clear lib tmp tsconfig.tsbuildinfo test.gjs.mjs test.node.mjs",
|
|
22
|
+
"check": "gjsify tsc --noEmit",
|
|
23
|
+
"build": "gjsify run build:gjsify && gjsify run build:types",
|
|
24
|
+
"build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}' 'src/test.gjs.mts' 'src/fake-goa.ts'",
|
|
25
|
+
"build:types": "gjsify tsc",
|
|
26
|
+
"build:test": "gjsify run build:test:gjs && gjsify run build:test:node",
|
|
27
|
+
"build:test:gjs": "gjsify build src/test.gjs.mts --app gjs --outfile test.gjs.mjs",
|
|
28
|
+
"build:test:node": "gjsify build src/test.mts --app node --outfile test.node.mjs",
|
|
29
|
+
"test": "gjsify run build:gjsify && gjsify run build:test && gjsify run test:node && gjsify run test:gjs",
|
|
30
|
+
"test:gjs": "gjsify env DBUS_SESSION_BUS_ADDRESS=unix:path=/nonexistent XDG_RUNTIME_DIR=/nonexistent gjsify run test.gjs.mjs",
|
|
31
|
+
"test:node": "node test.node.mjs"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"gjs",
|
|
35
|
+
"accounts",
|
|
36
|
+
"gnome-online-accounts",
|
|
37
|
+
"goa",
|
|
38
|
+
"mail",
|
|
39
|
+
"imap",
|
|
40
|
+
"smtp"
|
|
41
|
+
],
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@girs/gio-2.0": "5.4.0",
|
|
44
|
+
"@girs/gjs": "5.4.0",
|
|
45
|
+
"@girs/glib-2.0": "5.4.0",
|
|
46
|
+
"@girs/gobject-2.0": "5.4.0",
|
|
47
|
+
"@girs/goa-1.0": "5.4.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@gjsify/cli": "^0.55.0",
|
|
51
|
+
"@gjsify/unit": "^0.55.0",
|
|
52
|
+
"typescript": "^6.0.3"
|
|
53
|
+
},
|
|
54
|
+
"license": "MIT",
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "git+https://github.com/gjsify/gjsify.git",
|
|
58
|
+
"directory": "packages/gjs/system-accounts"
|
|
59
|
+
},
|
|
60
|
+
"bugs": {
|
|
61
|
+
"url": "https://github.com/gjsify/gjsify/issues"
|
|
62
|
+
},
|
|
63
|
+
"homepage": "https://github.com/gjsify/gjsify/tree/main/packages/gjs/system-accounts#readme",
|
|
64
|
+
"gjsify": {
|
|
65
|
+
"tier": 3,
|
|
66
|
+
"runtimes": {
|
|
67
|
+
"gjs": "native",
|
|
68
|
+
"node": "none",
|
|
69
|
+
"browser": "none",
|
|
70
|
+
"nativescript": "none"
|
|
71
|
+
},
|
|
72
|
+
"headless": [
|
|
73
|
+
"Gdk",
|
|
74
|
+
"GdkPixbuf",
|
|
75
|
+
"Gsk",
|
|
76
|
+
"Gtk",
|
|
77
|
+
"Adw"
|
|
78
|
+
],
|
|
79
|
+
"os": {
|
|
80
|
+
"linux": "supported",
|
|
81
|
+
"darwin": "none",
|
|
82
|
+
"win32": "none"
|
|
83
|
+
},
|
|
84
|
+
"osNotes": {
|
|
85
|
+
"darwin": "No driver: the package answers with empty capabilities (ADR 0095). macOS has no system-account API for third-party apps that was found; this is not proven either way.",
|
|
86
|
+
"win32": "No driver: the package answers with empty capabilities (ADR 0095). No system-account API for third-party apps was found; this is not proven either way."
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|