@holokai/neuron-sdk 0.1.1 → 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 +117 -0
- package/dist/auth.d.mts +187 -0
- package/dist/auth.d.ts +187 -0
- package/dist/auth.js +354 -0
- package/dist/auth.js.map +1 -0
- package/dist/auth.mjs +343 -0
- package/dist/auth.mjs.map +1 -0
- package/dist/cli.js +436 -0
- package/dist/cli.js.map +1 -0
- package/package.json +10 -1
package/README.md
CHANGED
|
@@ -50,6 +50,43 @@ process.on('SIGTERM', () => neuron.stop({ drain: true, timeoutMs: 30_000 }));
|
|
|
50
50
|
|
|
51
51
|
---
|
|
52
52
|
|
|
53
|
+
## Authentication
|
|
54
|
+
|
|
55
|
+
The `auth` callback supplies the bearer token for every POST and SSE open. How you obtain tokens depends on where the neuron runs (full model: `docs/NEURON_AUTH_SPEC.md` in the bigbrain repo):
|
|
56
|
+
|
|
57
|
+
- **Desktop-embedded neurons** run under the signed-in user: exchange the user's Moku JWT for an `aud=bigbrain` token (RFC 8693) and return it from `auth`.
|
|
58
|
+
- **Headless / enterprise neurons** enroll once via Moku's device flow, then mint short-lived tokens from the stored client credential — no static long-lived JWT anywhere.
|
|
59
|
+
|
|
60
|
+
### Enroll (one-time, headless neurons)
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx @holokai/neuron-sdk enroll --moku-url https://moku.example \
|
|
64
|
+
--name warehouse-connector --capabilities org/acme/erp.read,org/acme/erp.write
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Prints a verification URL + user code; an org admin approves in Moku. The client credential is written to `~/.holokai/neuron-credentials.json` (`0600`; override with `--credentials-file`). The requested capabilities are display hints for the approval screen — what the neuron may actually advertise is governed by the org's capability catalog. Polling is resilient: transient server errors (409/5xx) and network blips are retried until the device code expires; only a denial, expiry, or protocol error is terminal.
|
|
68
|
+
|
|
69
|
+
Also available programmatically: `enroll(opts)` from `@holokai/neuron-sdk/auth`.
|
|
70
|
+
|
|
71
|
+
### Steady state
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { Neuron } from '@holokai/neuron-sdk';
|
|
75
|
+
import { createClientCredentialsAuth, defaultCredentialPath } from '@holokai/neuron-sdk/auth';
|
|
76
|
+
|
|
77
|
+
const neuron = new Neuron({
|
|
78
|
+
gatewayUrl: 'https://api.example.com',
|
|
79
|
+
neuronId: 'warehouse-connector-001',
|
|
80
|
+
auth: createClientCredentialsAuth(defaultCredentialPath()),
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`createClientCredentialsAuth` mints `aud=bigbrain` tokens via the OAuth2 `client_credentials` grant, caches them against the response's `expires_in` (proactive refresh, single-flight), and re-reads the credential file on `invalid_client` so an operator-rotated secret is picked up without a restart. If the credential was revoked in Moku it throws `CredentialRevokedError` — re-enroll to recover.
|
|
85
|
+
|
|
86
|
+
> `@holokai/neuron-sdk/auth` is **Node-only** (it reads the credential file); the root and `./ipc` entries remain browser-clean. Python SDK parity for enrollment/minting is a planned follow-up in `holokai-neuron-sdk` — until then, Python neurons keep using an externally supplied token.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
53
90
|
## Core concepts
|
|
54
91
|
|
|
55
92
|
### Capability
|
|
@@ -778,3 +815,83 @@ Heartbeats aren't reaching the gateway, so the reaper hasn't reclaimed them. Che
|
|
|
778
815
|
### Auth 401 on every request
|
|
779
816
|
|
|
780
817
|
The `auth` callback isn't returning a valid token. If using a refresh flow, ensure the callback fetches a fresh token on each call (the SDK caches nothing — it's your callback's job).
|
|
818
|
+
|
|
819
|
+
---
|
|
820
|
+
|
|
821
|
+
## Releasing (maintainers)
|
|
822
|
+
|
|
823
|
+
`@holokai/neuron-sdk` is published to public npm. Releases are cut from a
|
|
824
|
+
monorepo-scoped git tag, and CI does the publish — there is no npm token on
|
|
825
|
+
anyone's laptop and none stored in the repo.
|
|
826
|
+
|
|
827
|
+
### Cut a release
|
|
828
|
+
|
|
829
|
+
1. **Bump the version** in `packages/neuron-sdk/package.json`
|
|
830
|
+
(`version`). Follow semver:
|
|
831
|
+
- Pilot phase, we are in **`0.x`**. Because the protocol is still absorbing
|
|
832
|
+
pilot-developer feedback, a **breaking** change bumps the **minor**
|
|
833
|
+
(`0.2.0` → `0.3.0`); a backward-compatible change or fix bumps the
|
|
834
|
+
**patch** (`0.2.0` → `0.2.1`). This is the standard `0.x` convention and
|
|
835
|
+
is what buys us room to break things on feedback without a major bump.
|
|
836
|
+
- Hold **`1.0.0`** until the public API has survived pilot feedback and we're
|
|
837
|
+
ready to commit to "breaking changes bump the major." Jumping to `1.0.0`
|
|
838
|
+
is a stability promise, not a milestone — don't spend it early.
|
|
839
|
+
2. **Commit** the bump (reference the Linear ticket) and merge to `main`.
|
|
840
|
+
3. **Tag and push** from `main`:
|
|
841
|
+
```sh
|
|
842
|
+
git tag neuron-sdk-v0.2.0 # must equal the package.json version
|
|
843
|
+
git push origin neuron-sdk-v0.2.0
|
|
844
|
+
```
|
|
845
|
+
The tag is prefixed `neuron-sdk-v` (the Python SDK uses
|
|
846
|
+
`neuron-sdk-python-v`) so the two never collide.
|
|
847
|
+
4. **`publish-neuron-sdk.yml`** fires on the tag: it asserts the tag version
|
|
848
|
+
matches `package.json`, runs the `prepublish` gate (`build` + `test`), and
|
|
849
|
+
publishes to npm. The `npm` GitHub Environment gates the publish behind a
|
|
850
|
+
**required-reviewer approval** — approve the run under Actions, and on success
|
|
851
|
+
the new version is live at
|
|
852
|
+
[`@holokai/neuron-sdk`](https://www.npmjs.com/package/@holokai/neuron-sdk).
|
|
853
|
+
|
|
854
|
+
> No `--provenance`: bigbrain is a private repo, so npm's provenance
|
|
855
|
+
> attestation (a public transparency-log link back to the source) adds little
|
|
856
|
+
> and has historically errored from private repos. Trusted-publishing auth is
|
|
857
|
+
> unaffected. Add the flag back if bigbrain ever goes public.
|
|
858
|
+
|
|
859
|
+
The tag→`package.json` assertion means a mismatched tag fails **before** the
|
|
860
|
+
registry is touched — npm refuses to re-upload an existing version, so there's
|
|
861
|
+
no half-published state to clean up.
|
|
862
|
+
|
|
863
|
+
### Publish auth: npm Trusted Publishing (one-time setup)
|
|
864
|
+
|
|
865
|
+
CI authenticates to npm via **OIDC trusted publishing** — GitHub Actions
|
|
866
|
+
exchanges a short-lived OIDC token for publish rights, so no long-lived
|
|
867
|
+
`NPM_TOKEN` exists to leak. This mirrors the Python SDK's PyPI trusted
|
|
868
|
+
publishing. It requires two one-time setups:
|
|
869
|
+
|
|
870
|
+
1. **On npmjs.com** (a package admin, once): open the
|
|
871
|
+
[`@holokai/neuron-sdk`](https://www.npmjs.com/package/@holokai/neuron-sdk)
|
|
872
|
+
package → **Settings → Trusted Publisher → GitHub Actions**, and register:
|
|
873
|
+
- Organization / repository: `holok-ai/bigbrain`
|
|
874
|
+
- Workflow filename: `publish-neuron-sdk.yml`
|
|
875
|
+
- Environment: `npm`
|
|
876
|
+
2. **In this repo** (once): create the **`npm`** GitHub Environment
|
|
877
|
+
(Settings → Environments). Add reviewers/branch protection here if you want a
|
|
878
|
+
manual approval gate before each publish.
|
|
879
|
+
|
|
880
|
+
Until the trusted publisher is registered, the `npm publish` step fails with an
|
|
881
|
+
auth error — that's the expected "not set up yet" signal, not a code bug.
|
|
882
|
+
|
|
883
|
+
### Manual publish (fallback / first bootstrap)
|
|
884
|
+
|
|
885
|
+
The tag flow is the norm. If you ever need to publish by hand (e.g. to bootstrap
|
|
886
|
+
the very first version before trusted publishing is configured):
|
|
887
|
+
|
|
888
|
+
```sh
|
|
889
|
+
cd packages/neuron-sdk
|
|
890
|
+
pnpm run publish:npm:dry # inspect the tarball + files list
|
|
891
|
+
pnpm run publish:npm # runs the prepublishOnly build+test gate, then publishes
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
This uses your own `npm login` credentials and skips the CI approval gate —
|
|
895
|
+
prefer the tag flow for anything a developer will consume.
|
|
896
|
+
|
|
897
|
+
<!-- ci: force full build on the self-hosted runner (HOL-2028 verify); safe to remove -->
|
package/dist/auth.d.mts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/** Function returning the current bearer JWT. Called on each POST and SSE
|
|
2
|
+
* open; can refresh tokens internally. The SDK retries once on 401, so
|
|
3
|
+
* refreshing on 401 inside the callback is the right place to do it. */
|
|
4
|
+
type AuthCallback = () => Promise<string> | string;
|
|
5
|
+
/** Logger surface — minimal shape so consumers can plug in pino, console,
|
|
6
|
+
* etc. without an interface mismatch. */
|
|
7
|
+
interface NeuronLogger {
|
|
8
|
+
debug(meta: Record<string, unknown>, msg?: string): void;
|
|
9
|
+
info(meta: Record<string, unknown>, msg?: string): void;
|
|
10
|
+
warn(meta: Record<string, unknown>, msg?: string): void;
|
|
11
|
+
error(meta: Record<string, unknown>, msg?: string): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* OAuth endpoint set resolved from RFC 8414 discovery (or the fixed
|
|
16
|
+
* fallback paths when discovery is unreachable — see `discovery.ts`).
|
|
17
|
+
*/
|
|
18
|
+
interface OAuthEndpoints {
|
|
19
|
+
tokenEndpoint: string;
|
|
20
|
+
deviceAuthorizationEndpoint: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The durable artifact on a neuron host: a revocable Moku OAuth client
|
|
24
|
+
* credential (never a bearer JWT). Field names mirror the wire/JSON
|
|
25
|
+
* shape so the file is greppable against Moku's own vocabulary.
|
|
26
|
+
*/
|
|
27
|
+
interface StoredCredential {
|
|
28
|
+
client_id: string;
|
|
29
|
+
client_secret: string;
|
|
30
|
+
token_endpoint: string;
|
|
31
|
+
moku_base_url: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* How the client proves its identity to Moku's token endpoint.
|
|
35
|
+
*
|
|
36
|
+
* v1 ships `client_secret_post` only (matching Moku's discovery doc).
|
|
37
|
+
* The interface exists so `private_key_jwt` (RFC 7523, HOL-2543) can
|
|
38
|
+
* slot in later without touching `createClientCredentialsAuth` callers:
|
|
39
|
+
* a keypair-backed implementation would append a signed
|
|
40
|
+
* `client_assertion` instead of a shared secret.
|
|
41
|
+
*/
|
|
42
|
+
interface ClientAuthenticator {
|
|
43
|
+
/** Apply client authentication to the token-endpoint form body. */
|
|
44
|
+
apply(form: URLSearchParams): void | Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
/** RFC 6749 §2.3.1-style shared-secret client auth, in the body. */
|
|
47
|
+
declare function clientSecretPost(clientId: string, clientSecret: string): ClientAuthenticator;
|
|
48
|
+
/** Successful token-endpoint response (RFC 6749 §5.1). On device-flow
|
|
49
|
+
* redemption Moku additionally delivers the client credential — once. */
|
|
50
|
+
interface TokenResponse {
|
|
51
|
+
access_token: string;
|
|
52
|
+
token_type: string;
|
|
53
|
+
expires_in: number;
|
|
54
|
+
scope?: string;
|
|
55
|
+
client_id?: string;
|
|
56
|
+
client_secret?: string;
|
|
57
|
+
}
|
|
58
|
+
/** Token-endpoint error (`{error, error_description}`, RFC 6749 §5.2). */
|
|
59
|
+
declare class OAuthTokenError extends Error {
|
|
60
|
+
readonly error: string;
|
|
61
|
+
readonly status: number;
|
|
62
|
+
constructor(error: string, status: number, description?: string);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Terminal auth failure: the stored credential no longer mints (revoked,
|
|
66
|
+
* deleted, or rotated away underneath us) or does not exist. The neuron
|
|
67
|
+
* must be re-enrolled; retrying against Moku is pointless and the SDK
|
|
68
|
+
* will not loop on it.
|
|
69
|
+
*/
|
|
70
|
+
declare class CredentialRevokedError extends Error {
|
|
71
|
+
constructor(message: string);
|
|
72
|
+
}
|
|
73
|
+
/** Enrollment (device flow) failed terminally. */
|
|
74
|
+
declare class EnrollmentError extends Error {
|
|
75
|
+
readonly code: 'expired_token' | 'access_denied' | 'invalid_response' | 'device_request_failed';
|
|
76
|
+
constructor(message: string, code: 'expired_token' | 'access_denied' | 'invalid_response' | 'device_request_failed');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface ClientCredentialsAuthOptions {
|
|
80
|
+
fetch?: typeof fetch;
|
|
81
|
+
logger?: NeuronLogger;
|
|
82
|
+
/**
|
|
83
|
+
* Refresh this long before the token's `expires_in` elapses. The
|
|
84
|
+
* response's `expires_in` is authoritative (Moku default is 15 min,
|
|
85
|
+
* per-audience cap 1h) — never assume a lifetime.
|
|
86
|
+
*/
|
|
87
|
+
refreshBufferMs?: number;
|
|
88
|
+
/**
|
|
89
|
+
* Client-auth method factory. Defaults to `client_secret_post` from the
|
|
90
|
+
* stored credential; a `private_key_jwt` implementation (HOL-2543) can
|
|
91
|
+
* be injected here without changing any other consumer code.
|
|
92
|
+
*/
|
|
93
|
+
authenticator?: (credential: StoredCredential) => ClientAuthenticator;
|
|
94
|
+
/** Clock override for tests. */
|
|
95
|
+
now?: () => number;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Steady-state auth for enrolled (headless/enterprise) neurons: returns an
|
|
99
|
+
* `auth()` callback for `HttpTransport` that mints short-lived
|
|
100
|
+
* `aud=bigbrain` access tokens from Moku via the OAuth2 `client_credentials`
|
|
101
|
+
* grant, using the credential file written by `enroll`.
|
|
102
|
+
*
|
|
103
|
+
* Behavior:
|
|
104
|
+
* - In-memory cache driven by the response's `expires_in`, with proactive
|
|
105
|
+
* refresh `refreshBufferMs` before expiry.
|
|
106
|
+
* - Single-flight: concurrent callers share one mint request.
|
|
107
|
+
* - On `invalid_client`, the credential file is re-read once before
|
|
108
|
+
* failing — an operator-rotated secret is picked up without a restart.
|
|
109
|
+
* - If the re-read credential still fails (revoked/deleted client), a
|
|
110
|
+
* `CredentialRevokedError` is thrown; the SDK's transport will not
|
|
111
|
+
* retry-loop against Moku on it. Re-enrollment is required.
|
|
112
|
+
*/
|
|
113
|
+
declare function createClientCredentialsAuth(credentialPath: string, opts?: ClientCredentialsAuthOptions): AuthCallback;
|
|
114
|
+
|
|
115
|
+
/** `~/.holokai/neuron-credentials.json` unless the caller overrides. */
|
|
116
|
+
declare function defaultCredentialPath(): string;
|
|
117
|
+
/**
|
|
118
|
+
* Read a stored credential. Returns null when the file does not exist
|
|
119
|
+
* (unenrolled); throws on a present-but-malformed file, which is an
|
|
120
|
+
* operator problem the caller must not paper over.
|
|
121
|
+
*/
|
|
122
|
+
declare function readCredentialFile(path: string): StoredCredential | null;
|
|
123
|
+
/** Write the credential with owner-only permissions (0700 dir, 0600 file). */
|
|
124
|
+
declare function writeCredentialFile(path: string, credential: StoredCredential): void;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Resolve OAuth endpoints for a Moku base URL via RFC 8414 discovery,
|
|
128
|
+
* falling back to the fixed default paths when the metadata document is
|
|
129
|
+
* unreachable or malformed.
|
|
130
|
+
*
|
|
131
|
+
* Served endpoint URLs are re-rooted onto `mokuBaseUrl` (path is taken
|
|
132
|
+
* from the document, origin from the caller): the metadata document
|
|
133
|
+
* derives its URLs from the inbound request URL, which behind a
|
|
134
|
+
* TLS-terminating proxy can name an origin the neuron cannot reach.
|
|
135
|
+
* The caller-supplied base URL is the one address known to work.
|
|
136
|
+
*/
|
|
137
|
+
declare function discoverEndpoints(mokuBaseUrl: string, opts?: {
|
|
138
|
+
fetch?: typeof fetch;
|
|
139
|
+
logger?: NeuronLogger;
|
|
140
|
+
}): Promise<OAuthEndpoints>;
|
|
141
|
+
|
|
142
|
+
interface EnrollOptions {
|
|
143
|
+
/** Moku base URL — the only required config (endpoints are discovered). */
|
|
144
|
+
mokuBaseUrl: string;
|
|
145
|
+
/** Display name shown to the approving admin. Required by Moku. */
|
|
146
|
+
clientName: string;
|
|
147
|
+
/** Display hint for the approval screen; defaults to `os.hostname()`. */
|
|
148
|
+
hostname?: string;
|
|
149
|
+
/**
|
|
150
|
+
* Capability types this neuron intends to serve — display hints for the
|
|
151
|
+
* approval screen ONLY. Authorization comes from the org's capability
|
|
152
|
+
* catalog, not from this list (requested ≠ granted).
|
|
153
|
+
*/
|
|
154
|
+
capabilityTypes?: string[];
|
|
155
|
+
/** Where to persist the credential. Default: `~/.holokai/neuron-credentials.json`. */
|
|
156
|
+
credentialPath?: string;
|
|
157
|
+
fetch?: typeof fetch;
|
|
158
|
+
logger?: NeuronLogger;
|
|
159
|
+
/** Where user-facing lines (verification URI + user code) go. Default: console.log. */
|
|
160
|
+
out?: (line: string) => void;
|
|
161
|
+
/** Sleep override for tests. */
|
|
162
|
+
sleep?: (ms: number) => Promise<void>;
|
|
163
|
+
}
|
|
164
|
+
interface EnrollResult {
|
|
165
|
+
status: 'already-enrolled' | 'enrolled';
|
|
166
|
+
clientId: string;
|
|
167
|
+
credentialPath: string;
|
|
168
|
+
/** Bootstrap token from the redemption response (only when freshly enrolled). */
|
|
169
|
+
accessToken?: string;
|
|
170
|
+
expiresIn?: number;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* One-time enrollment of a headless neuron via Moku's RFC 8628 device
|
|
174
|
+
* authorization grant.
|
|
175
|
+
*
|
|
176
|
+
* Flow: start a device authorization → print the verification URI +
|
|
177
|
+
* user code (container-log friendly) → poll the token endpoint until an
|
|
178
|
+
* org ADMIN approves in Moku → persist the client credential delivered
|
|
179
|
+
* in the redemption token response (delivered exactly once).
|
|
180
|
+
*
|
|
181
|
+
* Idempotent: if a credential file already exists and still mints, this
|
|
182
|
+
* is a no-op reporting `already-enrolled`. A revoked existing credential
|
|
183
|
+
* falls through to a fresh enrollment.
|
|
184
|
+
*/
|
|
185
|
+
declare function enroll(opts: EnrollOptions): Promise<EnrollResult>;
|
|
186
|
+
|
|
187
|
+
export { type ClientAuthenticator, type ClientCredentialsAuthOptions, CredentialRevokedError, type EnrollOptions, type EnrollResult, EnrollmentError, type OAuthEndpoints, OAuthTokenError, type StoredCredential, type TokenResponse, clientSecretPost, createClientCredentialsAuth, defaultCredentialPath, discoverEndpoints, enroll, readCredentialFile, writeCredentialFile };
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/** Function returning the current bearer JWT. Called on each POST and SSE
|
|
2
|
+
* open; can refresh tokens internally. The SDK retries once on 401, so
|
|
3
|
+
* refreshing on 401 inside the callback is the right place to do it. */
|
|
4
|
+
type AuthCallback = () => Promise<string> | string;
|
|
5
|
+
/** Logger surface — minimal shape so consumers can plug in pino, console,
|
|
6
|
+
* etc. without an interface mismatch. */
|
|
7
|
+
interface NeuronLogger {
|
|
8
|
+
debug(meta: Record<string, unknown>, msg?: string): void;
|
|
9
|
+
info(meta: Record<string, unknown>, msg?: string): void;
|
|
10
|
+
warn(meta: Record<string, unknown>, msg?: string): void;
|
|
11
|
+
error(meta: Record<string, unknown>, msg?: string): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* OAuth endpoint set resolved from RFC 8414 discovery (or the fixed
|
|
16
|
+
* fallback paths when discovery is unreachable — see `discovery.ts`).
|
|
17
|
+
*/
|
|
18
|
+
interface OAuthEndpoints {
|
|
19
|
+
tokenEndpoint: string;
|
|
20
|
+
deviceAuthorizationEndpoint: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The durable artifact on a neuron host: a revocable Moku OAuth client
|
|
24
|
+
* credential (never a bearer JWT). Field names mirror the wire/JSON
|
|
25
|
+
* shape so the file is greppable against Moku's own vocabulary.
|
|
26
|
+
*/
|
|
27
|
+
interface StoredCredential {
|
|
28
|
+
client_id: string;
|
|
29
|
+
client_secret: string;
|
|
30
|
+
token_endpoint: string;
|
|
31
|
+
moku_base_url: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* How the client proves its identity to Moku's token endpoint.
|
|
35
|
+
*
|
|
36
|
+
* v1 ships `client_secret_post` only (matching Moku's discovery doc).
|
|
37
|
+
* The interface exists so `private_key_jwt` (RFC 7523, HOL-2543) can
|
|
38
|
+
* slot in later without touching `createClientCredentialsAuth` callers:
|
|
39
|
+
* a keypair-backed implementation would append a signed
|
|
40
|
+
* `client_assertion` instead of a shared secret.
|
|
41
|
+
*/
|
|
42
|
+
interface ClientAuthenticator {
|
|
43
|
+
/** Apply client authentication to the token-endpoint form body. */
|
|
44
|
+
apply(form: URLSearchParams): void | Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
/** RFC 6749 §2.3.1-style shared-secret client auth, in the body. */
|
|
47
|
+
declare function clientSecretPost(clientId: string, clientSecret: string): ClientAuthenticator;
|
|
48
|
+
/** Successful token-endpoint response (RFC 6749 §5.1). On device-flow
|
|
49
|
+
* redemption Moku additionally delivers the client credential — once. */
|
|
50
|
+
interface TokenResponse {
|
|
51
|
+
access_token: string;
|
|
52
|
+
token_type: string;
|
|
53
|
+
expires_in: number;
|
|
54
|
+
scope?: string;
|
|
55
|
+
client_id?: string;
|
|
56
|
+
client_secret?: string;
|
|
57
|
+
}
|
|
58
|
+
/** Token-endpoint error (`{error, error_description}`, RFC 6749 §5.2). */
|
|
59
|
+
declare class OAuthTokenError extends Error {
|
|
60
|
+
readonly error: string;
|
|
61
|
+
readonly status: number;
|
|
62
|
+
constructor(error: string, status: number, description?: string);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Terminal auth failure: the stored credential no longer mints (revoked,
|
|
66
|
+
* deleted, or rotated away underneath us) or does not exist. The neuron
|
|
67
|
+
* must be re-enrolled; retrying against Moku is pointless and the SDK
|
|
68
|
+
* will not loop on it.
|
|
69
|
+
*/
|
|
70
|
+
declare class CredentialRevokedError extends Error {
|
|
71
|
+
constructor(message: string);
|
|
72
|
+
}
|
|
73
|
+
/** Enrollment (device flow) failed terminally. */
|
|
74
|
+
declare class EnrollmentError extends Error {
|
|
75
|
+
readonly code: 'expired_token' | 'access_denied' | 'invalid_response' | 'device_request_failed';
|
|
76
|
+
constructor(message: string, code: 'expired_token' | 'access_denied' | 'invalid_response' | 'device_request_failed');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface ClientCredentialsAuthOptions {
|
|
80
|
+
fetch?: typeof fetch;
|
|
81
|
+
logger?: NeuronLogger;
|
|
82
|
+
/**
|
|
83
|
+
* Refresh this long before the token's `expires_in` elapses. The
|
|
84
|
+
* response's `expires_in` is authoritative (Moku default is 15 min,
|
|
85
|
+
* per-audience cap 1h) — never assume a lifetime.
|
|
86
|
+
*/
|
|
87
|
+
refreshBufferMs?: number;
|
|
88
|
+
/**
|
|
89
|
+
* Client-auth method factory. Defaults to `client_secret_post` from the
|
|
90
|
+
* stored credential; a `private_key_jwt` implementation (HOL-2543) can
|
|
91
|
+
* be injected here without changing any other consumer code.
|
|
92
|
+
*/
|
|
93
|
+
authenticator?: (credential: StoredCredential) => ClientAuthenticator;
|
|
94
|
+
/** Clock override for tests. */
|
|
95
|
+
now?: () => number;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Steady-state auth for enrolled (headless/enterprise) neurons: returns an
|
|
99
|
+
* `auth()` callback for `HttpTransport` that mints short-lived
|
|
100
|
+
* `aud=bigbrain` access tokens from Moku via the OAuth2 `client_credentials`
|
|
101
|
+
* grant, using the credential file written by `enroll`.
|
|
102
|
+
*
|
|
103
|
+
* Behavior:
|
|
104
|
+
* - In-memory cache driven by the response's `expires_in`, with proactive
|
|
105
|
+
* refresh `refreshBufferMs` before expiry.
|
|
106
|
+
* - Single-flight: concurrent callers share one mint request.
|
|
107
|
+
* - On `invalid_client`, the credential file is re-read once before
|
|
108
|
+
* failing — an operator-rotated secret is picked up without a restart.
|
|
109
|
+
* - If the re-read credential still fails (revoked/deleted client), a
|
|
110
|
+
* `CredentialRevokedError` is thrown; the SDK's transport will not
|
|
111
|
+
* retry-loop against Moku on it. Re-enrollment is required.
|
|
112
|
+
*/
|
|
113
|
+
declare function createClientCredentialsAuth(credentialPath: string, opts?: ClientCredentialsAuthOptions): AuthCallback;
|
|
114
|
+
|
|
115
|
+
/** `~/.holokai/neuron-credentials.json` unless the caller overrides. */
|
|
116
|
+
declare function defaultCredentialPath(): string;
|
|
117
|
+
/**
|
|
118
|
+
* Read a stored credential. Returns null when the file does not exist
|
|
119
|
+
* (unenrolled); throws on a present-but-malformed file, which is an
|
|
120
|
+
* operator problem the caller must not paper over.
|
|
121
|
+
*/
|
|
122
|
+
declare function readCredentialFile(path: string): StoredCredential | null;
|
|
123
|
+
/** Write the credential with owner-only permissions (0700 dir, 0600 file). */
|
|
124
|
+
declare function writeCredentialFile(path: string, credential: StoredCredential): void;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Resolve OAuth endpoints for a Moku base URL via RFC 8414 discovery,
|
|
128
|
+
* falling back to the fixed default paths when the metadata document is
|
|
129
|
+
* unreachable or malformed.
|
|
130
|
+
*
|
|
131
|
+
* Served endpoint URLs are re-rooted onto `mokuBaseUrl` (path is taken
|
|
132
|
+
* from the document, origin from the caller): the metadata document
|
|
133
|
+
* derives its URLs from the inbound request URL, which behind a
|
|
134
|
+
* TLS-terminating proxy can name an origin the neuron cannot reach.
|
|
135
|
+
* The caller-supplied base URL is the one address known to work.
|
|
136
|
+
*/
|
|
137
|
+
declare function discoverEndpoints(mokuBaseUrl: string, opts?: {
|
|
138
|
+
fetch?: typeof fetch;
|
|
139
|
+
logger?: NeuronLogger;
|
|
140
|
+
}): Promise<OAuthEndpoints>;
|
|
141
|
+
|
|
142
|
+
interface EnrollOptions {
|
|
143
|
+
/** Moku base URL — the only required config (endpoints are discovered). */
|
|
144
|
+
mokuBaseUrl: string;
|
|
145
|
+
/** Display name shown to the approving admin. Required by Moku. */
|
|
146
|
+
clientName: string;
|
|
147
|
+
/** Display hint for the approval screen; defaults to `os.hostname()`. */
|
|
148
|
+
hostname?: string;
|
|
149
|
+
/**
|
|
150
|
+
* Capability types this neuron intends to serve — display hints for the
|
|
151
|
+
* approval screen ONLY. Authorization comes from the org's capability
|
|
152
|
+
* catalog, not from this list (requested ≠ granted).
|
|
153
|
+
*/
|
|
154
|
+
capabilityTypes?: string[];
|
|
155
|
+
/** Where to persist the credential. Default: `~/.holokai/neuron-credentials.json`. */
|
|
156
|
+
credentialPath?: string;
|
|
157
|
+
fetch?: typeof fetch;
|
|
158
|
+
logger?: NeuronLogger;
|
|
159
|
+
/** Where user-facing lines (verification URI + user code) go. Default: console.log. */
|
|
160
|
+
out?: (line: string) => void;
|
|
161
|
+
/** Sleep override for tests. */
|
|
162
|
+
sleep?: (ms: number) => Promise<void>;
|
|
163
|
+
}
|
|
164
|
+
interface EnrollResult {
|
|
165
|
+
status: 'already-enrolled' | 'enrolled';
|
|
166
|
+
clientId: string;
|
|
167
|
+
credentialPath: string;
|
|
168
|
+
/** Bootstrap token from the redemption response (only when freshly enrolled). */
|
|
169
|
+
accessToken?: string;
|
|
170
|
+
expiresIn?: number;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* One-time enrollment of a headless neuron via Moku's RFC 8628 device
|
|
174
|
+
* authorization grant.
|
|
175
|
+
*
|
|
176
|
+
* Flow: start a device authorization → print the verification URI +
|
|
177
|
+
* user code (container-log friendly) → poll the token endpoint until an
|
|
178
|
+
* org ADMIN approves in Moku → persist the client credential delivered
|
|
179
|
+
* in the redemption token response (delivered exactly once).
|
|
180
|
+
*
|
|
181
|
+
* Idempotent: if a credential file already exists and still mints, this
|
|
182
|
+
* is a no-op reporting `already-enrolled`. A revoked existing credential
|
|
183
|
+
* falls through to a fresh enrollment.
|
|
184
|
+
*/
|
|
185
|
+
declare function enroll(opts: EnrollOptions): Promise<EnrollResult>;
|
|
186
|
+
|
|
187
|
+
export { type ClientAuthenticator, type ClientCredentialsAuthOptions, CredentialRevokedError, type EnrollOptions, type EnrollResult, EnrollmentError, type OAuthEndpoints, OAuthTokenError, type StoredCredential, type TokenResponse, clientSecretPost, createClientCredentialsAuth, defaultCredentialPath, discoverEndpoints, enroll, readCredentialFile, writeCredentialFile };
|