@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 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://api.example.com',
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://api.example.com'
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://api.example.com',
476
+ gatewayUrl: 'https://bigbrain.holokai.dev',
417
477
  neuronId: 'my-worker-001',
418
478
  auth: () => process.env.GATEWAY_TOKEN!,
419
479
  // optional:
@@ -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 };