@holokai/neuron-sdk 0.2.0 → 0.4.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 +63 -3
- 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/dist/index.d.mts +21 -8
- package/dist/index.d.ts +21 -8
- package/dist/index.js +100 -29
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +100 -29
- package/dist/index.mjs.map +1 -1
- package/dist/ipc.d.mts +1 -1
- package/dist/ipc.d.ts +1 -1
- package/dist/{transport-QqkooS7K.d.mts → transport-Cs5tY2IO.d.mts} +91 -2
- package/dist/{transport-QqkooS7K.d.ts → transport-Cs5tY2IO.d.ts} +91 -2
- package/package.json +12 -3
package/README.md
CHANGED
|
@@ -21,6 +21,16 @@ pnpm add @holokai/neuron-sdk
|
|
|
21
21
|
|
|
22
22
|
Runs in **Node 20+** (uses the global `fetch` and `ReadableStream`) and **modern browsers** (Chromium, Firefox, Safari latest). The package has no `node:*` imports — Holokai Desktop / Electron and the in-browser neuron-tester both consume the same SDK build.
|
|
23
23
|
|
|
24
|
+
> **Gateway compatibility (protocol 1.1.0, HOL-2926).** Since protocol
|
|
25
|
+
> 1.1.0 the SDK advertises each capability's contract (`description` +
|
|
26
|
+
> input/output JSON Schemas) on the register frame. The gateway validates
|
|
27
|
+
> register frames **strictly**, so a gateway older than HOL-2926 rejects
|
|
28
|
+
> them with `INVALID_FRAME` — **deploy the gateway before publishing or
|
|
29
|
+
> rolling out any 1.1.0-protocol SDK (including neuron-tester OTA)**. A
|
|
30
|
+
> rejected registration leaves the SSE stream open but the neuron
|
|
31
|
+
> lease-less; the SDK surfaces it via the `error` event
|
|
32
|
+
> (`registration rejected by gateway: ...`) so apps can react.
|
|
33
|
+
|
|
24
34
|
---
|
|
25
35
|
|
|
26
36
|
## Quick start
|
|
@@ -29,7 +39,7 @@ Runs in **Node 20+** (uses the global `fetch` and `ReadableStream`) and **modern
|
|
|
29
39
|
import { Neuron } from '@holokai/neuron-sdk';
|
|
30
40
|
|
|
31
41
|
const neuron = new Neuron({
|
|
32
|
-
gatewayUrl: 'https://
|
|
42
|
+
gatewayUrl: 'https://bigbrain.holokai.dev',
|
|
33
43
|
neuronId: 'my-worker-001', // stable per process/device
|
|
34
44
|
auth: () => process.env.GATEWAY_TOKEN!, // or an async refresh function
|
|
35
45
|
});
|
|
@@ -50,6 +60,43 @@ process.on('SIGTERM', () => neuron.stop({ drain: true, timeoutMs: 30_000 }));
|
|
|
50
60
|
|
|
51
61
|
---
|
|
52
62
|
|
|
63
|
+
## Authentication
|
|
64
|
+
|
|
65
|
+
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):
|
|
66
|
+
|
|
67
|
+
- **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`.
|
|
68
|
+
- **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.
|
|
69
|
+
|
|
70
|
+
### Enroll (one-time, headless neurons)
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npx @holokai/neuron-sdk enroll --moku-url https://moku.example \
|
|
74
|
+
--name warehouse-connector --capabilities org/acme/erp.read,org/acme/erp.write
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
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.
|
|
78
|
+
|
|
79
|
+
Also available programmatically: `enroll(opts)` from `@holokai/neuron-sdk/auth`.
|
|
80
|
+
|
|
81
|
+
### Steady state
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { Neuron } from '@holokai/neuron-sdk';
|
|
85
|
+
import { createClientCredentialsAuth, defaultCredentialPath } from '@holokai/neuron-sdk/auth';
|
|
86
|
+
|
|
87
|
+
const neuron = new Neuron({
|
|
88
|
+
gatewayUrl: 'https://bigbrain.holokai.dev',
|
|
89
|
+
neuronId: 'warehouse-connector-001',
|
|
90
|
+
auth: createClientCredentialsAuth(defaultCredentialPath()),
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`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.
|
|
95
|
+
|
|
96
|
+
> `@holokai/neuron-sdk/auth` is **Node-only** (it reads the credential file); the root and `./ipc` entries remain browser-clean. Python SDK parity ships in `holokai-neuron-sdk` ≥ 0.2.0 (`holokai_neuron_sdk.auth`: `holokai-neuron-sdk enroll` + `create_client_credentials_auth`) — same credential file, interchangeable across both SDKs.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
53
100
|
## Core concepts
|
|
54
101
|
|
|
55
102
|
### Capability
|
|
@@ -133,7 +180,7 @@ interface NeuronCommonOptions {
|
|
|
133
180
|
|
|
134
181
|
// Convenience: SDK builds an HttpTransport for you. Most common path.
|
|
135
182
|
interface NeuronOptionsWithHttp extends NeuronCommonOptions {
|
|
136
|
-
gatewayUrl: string; // Base URL, e.g. 'https://
|
|
183
|
+
gatewayUrl: string; // Base URL, e.g. 'https://bigbrain.holokai.dev'
|
|
137
184
|
auth: AuthCallback; // Token provider (see below)
|
|
138
185
|
initialReconnectDelayMs?: number; // Default 500ms
|
|
139
186
|
maxReconnectDelayMs?: number; // Default 30_000 (30s)
|
|
@@ -176,6 +223,19 @@ type Handler<TInput = unknown, TOutput = unknown> = (
|
|
|
176
223
|
|
|
177
224
|
If `inputSchema` is provided and the lease input fails validation, the handler is never called — the SDK sends a `schema-mismatch` nack automatically. Same for `outputSchema` on the return value.
|
|
178
225
|
|
|
226
|
+
**Schema precedence.** For each of input and output the SDK picks exactly one validator:
|
|
227
|
+
|
|
228
|
+
1. The zod schema from `defineCapability`, when the capability was registered that way. JSON Schema can't express `.refine()`, `.transform()`, or `.preprocess()`, so the zod contract stays authoritative and its transforms are what the handler sees and what goes on the wire.
|
|
229
|
+
2. Otherwise the JSON Schema passed here on `CapabilityRegistration`.
|
|
230
|
+
|
|
231
|
+
A capability registered with neither is not validated client-side — its handler runs on whatever arrives.
|
|
232
|
+
|
|
233
|
+
The `inputSchema` / `outputSchema` that ride on the lease frame are **not** consulted. They can only ever be your own schemas handed back to you — the register frame carries no schemas, so the server's only source is the `POST /plan` handler catalog the client supplies. And the planner doesn't persist those onto task rows, so on real workflow tasks the lease frame's schemas are absent entirely; the only case where they arrive populated is the dev-only `/__dev__/inject-task` route used by neuron-tester, which echoes back what the capability registered.
|
|
234
|
+
|
|
235
|
+
> **This is the only validation that runs.** The gateway's ack-time check validates against that same (empty) `lease.outputSchema`, so it does not currently catch anything. If a capability declares no schema here, its input and output are unchecked end to end.
|
|
236
|
+
|
|
237
|
+
> **Note:** the Go and Python SDKs currently prefer the lease frame's schema and fall back to the registered one. TypeScript deliberately diverges here; the other two are expected to follow.
|
|
238
|
+
|
|
179
239
|
---
|
|
180
240
|
|
|
181
241
|
### `neuron.start()`
|
|
@@ -413,7 +473,7 @@ Default transport. SSE for inbound, HTTPS POST for outbound. Reconnects with exp
|
|
|
413
473
|
import { Neuron, HttpTransport } from '@holokai/neuron-sdk';
|
|
414
474
|
|
|
415
475
|
const transport = new HttpTransport({
|
|
416
|
-
gatewayUrl: 'https://
|
|
476
|
+
gatewayUrl: 'https://bigbrain.holokai.dev',
|
|
417
477
|
neuronId: 'my-worker-001',
|
|
418
478
|
auth: () => process.env.GATEWAY_TOKEN!,
|
|
419
479
|
// optional:
|
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 };
|