@onesi/genesis-cli 0.0.0-stage → 0.1.13
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 +36 -2
- package/bin/genesis.js +3 -0
- package/dist/auth.d.ts +30 -0
- package/dist/auth.js +273 -0
- package/dist/beacons.d.ts +3 -0
- package/dist/beacons.js +94 -0
- package/dist/device-login.d.ts +25 -0
- package/dist/device-login.js +173 -0
- package/dist/dials.d.ts +7 -0
- package/dist/dials.js +96 -0
- package/dist/fleets.d.ts +4 -0
- package/dist/fleets.js +72 -0
- package/dist/flocks.d.ts +3 -0
- package/dist/flocks.js +72 -0
- package/dist/function-input.d.ts +3 -0
- package/dist/function-input.js +49 -0
- package/dist/henry.d.ts +3 -0
- package/dist/henry.js +26 -0
- package/dist/hives-marketplace.d.ts +10 -0
- package/dist/hives-marketplace.js +29 -0
- package/dist/hives-node-transport.d.ts +6 -0
- package/dist/hives-node-transport.js +147 -0
- package/dist/hives-operations.d.ts +9 -0
- package/dist/hives-operations.js +61 -0
- package/dist/hives-resources.d.ts +9 -0
- package/dist/hives-resources.js +94 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +640 -0
- package/dist/lifeid-native.d.ts +24 -0
- package/dist/lifeid-native.js +113 -0
- package/dist/minds-owner.d.ts +3 -0
- package/dist/minds-owner.js +42 -0
- package/dist/minds.d.ts +3 -0
- package/dist/minds.js +90 -0
- package/dist/output.d.ts +28 -0
- package/dist/output.js +46 -0
- package/dist/products.d.ts +14 -0
- package/dist/products.js +85 -0
- package/dist/relays-enrollment.d.ts +3 -0
- package/dist/relays-enrollment.js +7 -0
- package/dist/runner.d.ts +4 -0
- package/dist/runner.js +55 -0
- package/dist/secret-input.d.ts +14 -0
- package/dist/secret-input.js +85 -0
- package/dist/store.d.ts +39 -0
- package/dist/store.js +33 -0
- package/dist/wasm-input.d.ts +2 -0
- package/dist/wasm-input.js +31 -0
- package/package.json +51 -3
package/README.md
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @onesi/genesis-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`genesis`, the OneOS command line. It works against the Genesis API at `https://os.one.ml/api`.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx -y @onesi/genesis-cli@0.1.13 --help
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Sign in
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
genesis login # opens your browser to sign in with One
|
|
13
|
+
genesis login --device # on a machine without a browser: approve a code from any signed-in browser
|
|
14
|
+
genesis whoami
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
For scripts and CI, skip login and set an organization API key from the Console (https://os.one.ml → API keys):
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
GENESIS_API_KEY=sk_... genesis hives projects list
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Use
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
genesis status # live state of each service
|
|
27
|
+
genesis hives projects list
|
|
28
|
+
genesis hives projects create my-app --kind worker
|
|
29
|
+
genesis hives projects delete prj_...
|
|
30
|
+
genesis --json silos projects list # machine-readable output; errors include code, required_scope and request_id
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`genesis <service> --help` lists every command for a service. `--base-url` or `GENESIS_BASE_URL` overrides the gateway, and `https://console.life.one/api` is an alias. Requires Node.js 20.19 or later.
|
|
34
|
+
|
|
35
|
+
Use the same version as [`@onesi/genesis`](https://www.npmjs.com/package/@onesi/genesis) and [`@onesi/genesis-mcp`](https://www.npmjs.com/package/@onesi/genesis-mcp).
|
|
36
|
+
|
|
37
|
+
Docs: https://os.one.ml/docs · Error codes: https://os.one.ml/api/v1/errors
|
package/bin/genesis.js
ADDED
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type GenesisContextSelection } from '@onesi/genesis';
|
|
2
|
+
import type { CredentialStore, SavedLogin } from './store.js';
|
|
3
|
+
import { type NativeAttempt, type NativeRegistration } from './lifeid-native.js';
|
|
4
|
+
/** One account's admitted native product bridge, never a direct AuthKit bearer. */
|
|
5
|
+
export interface AuthConfig {
|
|
6
|
+
productId?: string;
|
|
7
|
+
redirectUri?: string;
|
|
8
|
+
audience?: string;
|
|
9
|
+
authApi: string;
|
|
10
|
+
fetch: typeof fetch;
|
|
11
|
+
sleep?: (ms: number) => Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
/** One account's registration for the Genesis CLI (product `genesis-cli`, its one loopback callback, the platform audience). */
|
|
14
|
+
export declare const GENESIS_CLI_REGISTRATION: NativeRegistration;
|
|
15
|
+
/** The registered profile by default; GENESIS_AUTH_* override it (an empty value disables browser login). */
|
|
16
|
+
export declare function authConfig(env?: NodeJS.ProcessEnv, f?: typeof fetch): AuthConfig;
|
|
17
|
+
export declare function startLogin(c: AuthConfig, options?: {
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
organizationHint?: string;
|
|
21
|
+
}): Promise<NativeAttempt>;
|
|
22
|
+
/** Scheduling/consistency only. The One account server and Genesis verifier own authority. */
|
|
23
|
+
export declare function jwtExp(token: string): number | undefined;
|
|
24
|
+
export declare function parseLifeSession(c: AuthConfig, value: unknown, now?: number): SavedLogin;
|
|
25
|
+
export declare function completeLogin(c: AuthConfig, attempt: NativeAttempt): Promise<SavedLogin>;
|
|
26
|
+
export declare function refresh(c: AuthConfig, s: SavedLogin, organizationId?: string): Promise<SavedLogin>;
|
|
27
|
+
/** Commit both replacement credentials through the existing atomic keychain entry. */
|
|
28
|
+
export declare function selectSavedContext(c: AuthConfig, store: CredentialStore, selection: GenesisContextSelection, signal?: AbortSignal): Promise<SavedLogin>;
|
|
29
|
+
/** OS-keychain bridge session; one committed rotation per store, never raw AuthKit. */
|
|
30
|
+
export declare function savedLoginProvider(c: AuthConfig, store: CredentialStore, now?: () => number): () => Promise<string | undefined>;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { selectGenesisContext } from '@onesi/genesis';
|
|
2
|
+
import { refreshDeviceLogin, requireDeviceSaved } from './device-login.js';
|
|
3
|
+
import { LIFEID_ISSUER, startNativeAttempt, validateRegistration } from './lifeid-native.js';
|
|
4
|
+
/** One account's registration for the Genesis CLI (product `genesis-cli`, its one loopback callback, the platform audience). */
|
|
5
|
+
export const GENESIS_CLI_REGISTRATION = {
|
|
6
|
+
productId: 'genesis-cli', redirectUri: 'http://127.0.0.1:53682/lifeid/callback', audience: 'l1fe-platform',
|
|
7
|
+
};
|
|
8
|
+
/** The registered profile by default; GENESIS_AUTH_* override it (an empty value disables browser login). */
|
|
9
|
+
export function authConfig(env = process.env, f = fetch) {
|
|
10
|
+
return { productId: env.GENESIS_AUTH_PRODUCT ?? GENESIS_CLI_REGISTRATION.productId,
|
|
11
|
+
redirectUri: env.GENESIS_AUTH_REDIRECT_URI ?? GENESIS_CLI_REGISTRATION.redirectUri,
|
|
12
|
+
audience: env.GENESIS_AUTH_AUDIENCE ?? GENESIS_CLI_REGISTRATION.audience, authApi: LIFEID_ISSUER, fetch: f };
|
|
13
|
+
}
|
|
14
|
+
function registration(c) {
|
|
15
|
+
if (c.authApi !== LIFEID_ISSUER || !c.productId || !c.redirectUri || !c.audience) {
|
|
16
|
+
throw new Error('One account native login is not admitted here. Configure GENESIS_AUTH_PRODUCT, GENESIS_AUTH_REDIRECT_URI and GENESIS_AUTH_AUDIENCE from the registered One account profile, or use GENESIS_API_KEY.');
|
|
17
|
+
}
|
|
18
|
+
const r = { productId: c.productId, redirectUri: c.redirectUri, audience: c.audience };
|
|
19
|
+
validateRegistration(r);
|
|
20
|
+
return r;
|
|
21
|
+
}
|
|
22
|
+
export async function startLogin(c, options = {}) {
|
|
23
|
+
return startNativeAttempt(registration(c), options);
|
|
24
|
+
}
|
|
25
|
+
function object(v) {
|
|
26
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v) ? v : undefined;
|
|
27
|
+
}
|
|
28
|
+
function tokenClaims(token) {
|
|
29
|
+
try {
|
|
30
|
+
const parts = token.split('.');
|
|
31
|
+
if (parts.length !== 3 || !parts.every(p => /^[A-Za-z0-9_-]+$/.test(p)) || token.length > 16_384)
|
|
32
|
+
return undefined;
|
|
33
|
+
return object(JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8')));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Scheduling/consistency only. The One account server and Genesis verifier own authority. */
|
|
40
|
+
export function jwtExp(token) {
|
|
41
|
+
const exp = tokenClaims(token)?.exp;
|
|
42
|
+
return typeof exp === 'number' && Number.isSafeInteger(exp) ? exp : undefined;
|
|
43
|
+
}
|
|
44
|
+
function id(v, prefix) {
|
|
45
|
+
return typeof v === 'string' && new RegExp(`^${prefix}_[0-9A-HJKMNP-TV-Z]{26}$`).test(v);
|
|
46
|
+
}
|
|
47
|
+
function secret(v) {
|
|
48
|
+
return typeof v === 'string' && v.length > 0 && v.length <= 16_384 && !/[\s\x00-\x1f\x7f]/.test(v);
|
|
49
|
+
}
|
|
50
|
+
class LifeIdRequestError extends Error {
|
|
51
|
+
}
|
|
52
|
+
function invalidEnvelope() { throw new LifeIdRequestError('One account returned an invalid or mismatched session. Run `genesis login` again.'); }
|
|
53
|
+
export function parseLifeSession(c, value, now = Date.now() / 1000) {
|
|
54
|
+
const r = registration(c);
|
|
55
|
+
const body = object(value), s = object(body?.session), p = object(body?.principal);
|
|
56
|
+
if (!s || !p || p.kind !== 'human' || !id(p.id, 'h') || s.principalKind !== 'human'
|
|
57
|
+
|| !id(s.id, 'sess') || !id(s.organizationId, 'o') || s.issuer !== LIFEID_ISSUER
|
|
58
|
+
|| s.audience !== r.audience || !secret(s.accessToken) || !secret(body?.refresh_token)
|
|
59
|
+
|| typeof s.issuedAt !== 'string' || typeof s.expiresAt !== 'string')
|
|
60
|
+
invalidEnvelope();
|
|
61
|
+
const issued = Date.parse(s.issuedAt) / 1000, expires = Date.parse(s.expiresAt) / 1000;
|
|
62
|
+
const claims = tokenClaims(s.accessToken);
|
|
63
|
+
const exp = jwtExp(s.accessToken);
|
|
64
|
+
const audiences = typeof claims?.aud === 'string' ? [claims.aud] : claims?.aud;
|
|
65
|
+
if (!Number.isFinite(issued) || !Number.isFinite(expires) || issued > now + 60 || expires <= now || expires <= issued
|
|
66
|
+
|| !claims || !exp || exp <= now || claims.iss !== LIFEID_ISSUER || !Array.isArray(audiences)
|
|
67
|
+
|| !audiences.every(a => typeof a === 'string') || !audiences.includes(r.audience)
|
|
68
|
+
|| claims.kind !== 'human' || claims.sid !== s.id || claims.sub !== p.id || claims.tid !== p.id || claims.org !== s.organizationId
|
|
69
|
+
|| (s.tenantId !== undefined && s.tenantId !== null && s.tenantId !== p.id)
|
|
70
|
+
|| claims.cnf !== undefined || claims.cap !== undefined || claims.obo !== undefined)
|
|
71
|
+
invalidEnvelope();
|
|
72
|
+
// The One account scope is the principal; the selected organization stays separate.
|
|
73
|
+
// Optional response projections are not required to retain committed rotation.
|
|
74
|
+
return { access_token: s.accessToken, refresh_token: body.refresh_token,
|
|
75
|
+
user: { id: p.id, ...(typeof p.email === 'string' ? { email: p.email } : {}) },
|
|
76
|
+
organization_id: s.organizationId, expires_at: Math.min(expires, exp), client_id: r.productId,
|
|
77
|
+
auth: { kind: 'lifeid-bridge', issuer: LIFEID_ISSUER, audience: r.audience, product_id: r.productId,
|
|
78
|
+
redirect_uri: r.redirectUri, principal_id: p.id, session_id: s.id } };
|
|
79
|
+
}
|
|
80
|
+
/** Bound headers AND body, forbid redirects, and never echo a provider payload. */
|
|
81
|
+
async function post(c, path, body, signal) {
|
|
82
|
+
registration(c);
|
|
83
|
+
if (signal?.aborted)
|
|
84
|
+
throw new LifeIdRequestError('One account request cancelled. No automatic retry was made.');
|
|
85
|
+
const controller = new AbortController();
|
|
86
|
+
let reader;
|
|
87
|
+
let response;
|
|
88
|
+
let rejectDeadline;
|
|
89
|
+
const deadline = new Promise((_, reject) => { rejectDeadline = reject; });
|
|
90
|
+
const abort = () => { controller.abort(); rejectDeadline(new LifeIdRequestError('One account request cancelled. No automatic retry was made.')); };
|
|
91
|
+
const timer = setTimeout(() => {
|
|
92
|
+
controller.abort();
|
|
93
|
+
rejectDeadline(new LifeIdRequestError('One account request timed out. No automatic retry was made.'));
|
|
94
|
+
}, 15_000);
|
|
95
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
96
|
+
if (signal?.aborted)
|
|
97
|
+
abort();
|
|
98
|
+
try {
|
|
99
|
+
return await Promise.race([deadline, (async () => {
|
|
100
|
+
if (controller.signal.aborted)
|
|
101
|
+
throw new LifeIdRequestError('One account request cancelled.');
|
|
102
|
+
response = await c.fetch(`${LIFEID_ISSUER}${path}`, { method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
103
|
+
body: JSON.stringify(body), redirect: 'error', cache: 'no-store', signal: controller.signal });
|
|
104
|
+
if (controller.signal.aborted) {
|
|
105
|
+
void response.body?.cancel().catch(() => { });
|
|
106
|
+
throw new LifeIdRequestError('One account request cancelled.');
|
|
107
|
+
}
|
|
108
|
+
if (!response.ok || !/^application\/json(?:;|$)/i.test(response.headers.get('content-type') ?? '')) {
|
|
109
|
+
void response.body?.cancel().catch(() => { });
|
|
110
|
+
throw new LifeIdRequestError(`One account request failed (${response.status}). Run \`genesis login\` again.`);
|
|
111
|
+
}
|
|
112
|
+
reader = response.body?.getReader();
|
|
113
|
+
if (!reader)
|
|
114
|
+
invalidEnvelope();
|
|
115
|
+
const chunks = [];
|
|
116
|
+
let size = 0;
|
|
117
|
+
while (true) {
|
|
118
|
+
const part = await reader.read();
|
|
119
|
+
if (part.done)
|
|
120
|
+
break;
|
|
121
|
+
size += part.value.byteLength;
|
|
122
|
+
if (size > 32_768) {
|
|
123
|
+
void reader.cancel().catch(() => { });
|
|
124
|
+
invalidEnvelope();
|
|
125
|
+
}
|
|
126
|
+
chunks.push(part.value);
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
invalidEnvelope();
|
|
133
|
+
}
|
|
134
|
+
})()]);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (error instanceof LifeIdRequestError)
|
|
138
|
+
throw error;
|
|
139
|
+
throw new Error('One account transport failed. No automatic retry was made.');
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
signal?.removeEventListener('abort', abort);
|
|
144
|
+
if (controller.signal.aborted)
|
|
145
|
+
void reader?.cancel().catch(() => { });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export async function completeLogin(c, attempt) {
|
|
149
|
+
try {
|
|
150
|
+
const r = registration(c);
|
|
151
|
+
if (r.productId !== attempt.registration.productId || r.redirectUri !== attempt.registration.redirectUri
|
|
152
|
+
|| r.audience !== attempt.registration.audience)
|
|
153
|
+
throw new Error('One account login registration changed.');
|
|
154
|
+
const code = await attempt.code;
|
|
155
|
+
const body = await post(c, '/v4/lifeid/exchange', { life_code: code, product_id: r.productId,
|
|
156
|
+
redirect_uri: r.redirectUri, code_verifier: attempt.codeVerifier }, attempt.signal);
|
|
157
|
+
return parseLifeSession(c, body);
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
attempt.close();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function requireSaved(c, s) {
|
|
164
|
+
if (s.auth?.kind === 'lifeid-device') {
|
|
165
|
+
requireDeviceSaved(s);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const r = registration(c), a = s.auth;
|
|
169
|
+
if (!a || a.kind !== 'lifeid-bridge' || a.issuer !== LIFEID_ISSUER || a.audience !== r.audience
|
|
170
|
+
|| a.product_id !== r.productId || a.redirect_uri !== r.redirectUri || s.client_id !== r.productId
|
|
171
|
+
|| !id(s.organization_id, 'o') || !id(a.principal_id, 'h') || !id(a.session_id, 'sess')) {
|
|
172
|
+
throw new Error('Saved login is not an admitted One account bridge session. Run `genesis login` again.');
|
|
173
|
+
}
|
|
174
|
+
const claims = tokenClaims(s.access_token), aud = claims?.aud;
|
|
175
|
+
if (!claims || claims.iss !== a.issuer || !(aud === a.audience || (Array.isArray(aud) && aud.includes(a.audience)))
|
|
176
|
+
|| claims.kind !== 'human' || claims.sid !== a.session_id || claims.sub !== a.principal_id || claims.tid !== a.principal_id
|
|
177
|
+
|| claims.org !== s.organization_id || claims.cnf !== undefined || claims.cap !== undefined || claims.obo !== undefined) {
|
|
178
|
+
throw new Error('Saved One account session is inconsistent. Run `genesis login` again.');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
export async function refresh(c, s, organizationId) {
|
|
182
|
+
requireSaved(c, s);
|
|
183
|
+
if (s.auth?.kind === 'lifeid-device') {
|
|
184
|
+
if (organizationId && organizationId !== s.organization_id) {
|
|
185
|
+
throw new Error('Changing organization requires a new verified One account login; refresh cannot select an organization.');
|
|
186
|
+
}
|
|
187
|
+
return refreshDeviceLogin(c, s);
|
|
188
|
+
}
|
|
189
|
+
if (organizationId && organizationId !== s.organization_id) {
|
|
190
|
+
throw new Error('Changing organization requires a new verified One account login; refresh cannot select an organization.');
|
|
191
|
+
}
|
|
192
|
+
if (!secret(s.refresh_token))
|
|
193
|
+
throw new Error('Session expired. Run `genesis login`.');
|
|
194
|
+
const n = parseLifeSession(c, await post(c, '/v4/auth/refresh', { refresh_token: s.refresh_token }));
|
|
195
|
+
if (n.organization_id !== s.organization_id || n.auth?.principal_id !== s.auth?.principal_id)
|
|
196
|
+
invalidEnvelope();
|
|
197
|
+
return n;
|
|
198
|
+
}
|
|
199
|
+
const rotations = new WeakMap();
|
|
200
|
+
/** Queue before the first await, so parallel selection and refresh cannot use
|
|
201
|
+
* the same spent credential. A failed predecessor does not trigger a retry. */
|
|
202
|
+
function serialize(store, run) {
|
|
203
|
+
const previous = rotations.get(store);
|
|
204
|
+
const pending = previous ? previous.then(run) : Promise.resolve().then(run);
|
|
205
|
+
rotations.set(store, pending);
|
|
206
|
+
return pending.finally(() => { if (rotations.get(store) === pending)
|
|
207
|
+
rotations.delete(store); });
|
|
208
|
+
}
|
|
209
|
+
async function commit(store, value) {
|
|
210
|
+
try {
|
|
211
|
+
await store.set(value);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
throw new Error('The returned One account session could not be saved. No automatic retry was made.');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function currentLogin(c, store) {
|
|
218
|
+
const saved = await store.get();
|
|
219
|
+
if (!saved)
|
|
220
|
+
throw new Error('Run `genesis login` before selecting a context.');
|
|
221
|
+
requireSaved(c, saved);
|
|
222
|
+
return saved;
|
|
223
|
+
}
|
|
224
|
+
function needsRefresh(saved, now) {
|
|
225
|
+
return Math.min(saved.expires_at ?? 0, jwtExp(saved.access_token) ?? 0) - 60 <= now;
|
|
226
|
+
}
|
|
227
|
+
/** Commit both replacement credentials through the existing atomic keychain entry. */
|
|
228
|
+
export async function selectSavedContext(c, store, selection, signal) {
|
|
229
|
+
const requested = { ...selection };
|
|
230
|
+
return serialize(store, async () => {
|
|
231
|
+
let saved;
|
|
232
|
+
const response = await selectGenesisContext(requested, { signal, fetch: c.fetch, credential: async () => {
|
|
233
|
+
signal?.throwIfAborted();
|
|
234
|
+
saved = await currentLogin(c, store);
|
|
235
|
+
if (needsRefresh(saved, Date.now() / 1000)) {
|
|
236
|
+
saved = await refresh(c, saved);
|
|
237
|
+
await commit(store, saved);
|
|
238
|
+
}
|
|
239
|
+
signal?.throwIfAborted();
|
|
240
|
+
return saved.access_token;
|
|
241
|
+
} });
|
|
242
|
+
const next = parseLifeSession(c, response);
|
|
243
|
+
if (next.auth?.principal_id !== saved?.auth?.principal_id || next.organization_id !== requested.organizationId)
|
|
244
|
+
invalidEnvelope();
|
|
245
|
+
await commit(store, next);
|
|
246
|
+
return next;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/** OS-keychain bridge session; one committed rotation per store, never raw AuthKit. */
|
|
250
|
+
export function savedLoginProvider(c, store, now = () => Date.now() / 1000) {
|
|
251
|
+
return async () => {
|
|
252
|
+
const mutation = rotations.get(store);
|
|
253
|
+
if (mutation)
|
|
254
|
+
return (await mutation).access_token;
|
|
255
|
+
const saved = await store.get();
|
|
256
|
+
if (!saved)
|
|
257
|
+
return undefined;
|
|
258
|
+
requireSaved(c, saved);
|
|
259
|
+
const newer = rotations.get(store);
|
|
260
|
+
if (newer)
|
|
261
|
+
return (await newer).access_token;
|
|
262
|
+
if (!needsRefresh(saved, now()))
|
|
263
|
+
return saved.access_token;
|
|
264
|
+
return (await serialize(store, async () => {
|
|
265
|
+
const current = await currentLogin(c, store);
|
|
266
|
+
if (!needsRefresh(current, now()))
|
|
267
|
+
return current;
|
|
268
|
+
const next = await refresh(c, current);
|
|
269
|
+
await commit(store, next);
|
|
270
|
+
return next;
|
|
271
|
+
})).access_token;
|
|
272
|
+
};
|
|
273
|
+
}
|
package/dist/beacons.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { createPrivateKey, sign } from 'node:crypto';
|
|
3
|
+
export function registerBeacons(program, client, emit) {
|
|
4
|
+
const beacons = program.command('beacons').description('Beacons: Agent-native mesh');
|
|
5
|
+
const fleets = beacons.command('fleets').description('Fleet metadata and policy in your verified selection');
|
|
6
|
+
fleets.command('list').description('List actual fleets in the current authorized Sandbox scope').action(async () => {
|
|
7
|
+
const result = await (await client()).beacons.fleets.list();
|
|
8
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
9
|
+
});
|
|
10
|
+
fleets.command('rename <fleet-id>').requiredOption('--name <name>', 'Fleet name, at most 128 UTF-8 bytes').action(async (id, opts) => {
|
|
11
|
+
const result = await (await client()).beacons.fleets.update(id, { name: opts.name });
|
|
12
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
13
|
+
});
|
|
14
|
+
fleets.command('peers <fleet-id>').description('List actual peers, including durable revocation state').action(async (id) => {
|
|
15
|
+
const result = await (await client()).beacons.fleets.peers(id);
|
|
16
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
17
|
+
});
|
|
18
|
+
const policy = fleets.command('policy').description('Future enrollment admission; does not grant permissions or revoke devices');
|
|
19
|
+
policy.command('get <fleet-id>').action(async (id) => {
|
|
20
|
+
const result = await (await client()).beacons.fleets.policy.get(id);
|
|
21
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
22
|
+
});
|
|
23
|
+
policy.command('set <fleet-id>').requiredOption('--device-enrollment <mode>', 'allowed_with_permission or paused')
|
|
24
|
+
.option('--max-devices <count>', 'Integer 1–10000; omit to save the policy without a device cap')
|
|
25
|
+
.action(async (id, opts) => {
|
|
26
|
+
if (opts.deviceEnrollment !== 'allowed_with_permission' && opts.deviceEnrollment !== 'paused')
|
|
27
|
+
throw new Error('Invalid device enrollment mode.');
|
|
28
|
+
if (opts.maxDevices !== undefined && !/^[1-9][0-9]{0,4}$/.test(opts.maxDevices))
|
|
29
|
+
throw new Error('Invalid maximum device count.');
|
|
30
|
+
const result = await (await client()).beacons.fleets.policy.update(id, {
|
|
31
|
+
device_enrollment: opts.deviceEnrollment,
|
|
32
|
+
...(opts.maxDevices === undefined ? {} : { max_devices: Number(opts.maxDevices) }),
|
|
33
|
+
});
|
|
34
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
35
|
+
});
|
|
36
|
+
fleets.command('provision').requiredOption('--request-id <uuid>', 'Stable UUID; reuse with the same name on retry')
|
|
37
|
+
.option('--name <name>', 'Optional fleet name')
|
|
38
|
+
.action(async (opts) => {
|
|
39
|
+
const result = await (await client()).beacons.fleets.provision(opts.requestId, opts.name === undefined ? {} : { name: opts.name });
|
|
40
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
41
|
+
});
|
|
42
|
+
const devices = beacons.command('devices').description('Manage Bridge devices in your signed-in organization');
|
|
43
|
+
devices.command('challenge <fleet-id>').requiredOption('--proof-key <key>', 'Base64url Ed25519 public key').requiredOption('--tunnel-key <key>', 'Base64url WireGuard public key').option('--transport <transport>', 't1_full or t2_sandboxed', 't1_full')
|
|
44
|
+
.action(async (fleetId, opts) => {
|
|
45
|
+
if (opts.transport !== 't1_full' && opts.transport !== 't2_sandboxed')
|
|
46
|
+
throw new Error('Unsupported device transport.');
|
|
47
|
+
const result = await (await client()).beacons.devices.challenge({ fleet_id: fleetId, proof_key: opts.proofKey, tunnel_key: opts.tunnelKey, transport: opts.transport });
|
|
48
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
49
|
+
});
|
|
50
|
+
devices.command('enroll <challenge-file>').requiredOption('--key-file <path>', 'Local Ed25519 PEM private key; never uploaded')
|
|
51
|
+
.action(async (challengeFile, opts) => {
|
|
52
|
+
const challenge = JSON.parse(await readFile(challengeFile, 'utf8'));
|
|
53
|
+
const fields = ['version', 'id', 'nonce', 'issuer', 'organization', 'principal', 'workspace', 'project', 'environment', 'binding_revision', 'transport', 'fleet_id', 'proof_key', 'tunnel_key', 'issued_at', 'expires_at'];
|
|
54
|
+
if (Object.keys(challenge).length !== fields.length || fields.some(field => challenge[field] === undefined))
|
|
55
|
+
throw new Error('Invalid challenge file.');
|
|
56
|
+
const canonical = Object.fromEntries(fields.map(field => [field, challenge[field]]));
|
|
57
|
+
const key = createPrivateKey(await readFile(opts.keyFile));
|
|
58
|
+
if (key.asymmetricKeyType !== 'ed25519')
|
|
59
|
+
throw new Error('An Ed25519 device key is required.');
|
|
60
|
+
const signature = sign(null, Buffer.from('Genesis Beacons device enrollment v1\n' + JSON.stringify(canonical)), key).toString('base64url');
|
|
61
|
+
const result = await (await client()).beacons.devices.enroll({ challenge, signature });
|
|
62
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
63
|
+
});
|
|
64
|
+
devices.command('revocations <id>').description('Poll durable transport teardown commands').action(async (id) => {
|
|
65
|
+
const result = await (await client()).beacons.devices.revocations(id);
|
|
66
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
67
|
+
});
|
|
68
|
+
devices.command('ack <id> <version>').requiredOption('--action <action>', 'Applied interface_torn_down or peer_key_removed; apply transport action first').action(async (id, version, opts) => {
|
|
69
|
+
const numeric = Number(version);
|
|
70
|
+
if (!Number.isSafeInteger(numeric) || numeric < 1)
|
|
71
|
+
throw new Error('Invalid revocation version.');
|
|
72
|
+
if (opts.action !== 'interface_torn_down' && opts.action !== 'peer_key_removed')
|
|
73
|
+
throw new Error('Invalid transport action.');
|
|
74
|
+
const result = await (await client()).beacons.devices.acknowledge(id, numeric, { action: opts.action });
|
|
75
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
76
|
+
});
|
|
77
|
+
devices.command('list', { isDefault: true }).action(async () => {
|
|
78
|
+
const result = await (await client()).beacons.devices.list();
|
|
79
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
80
|
+
});
|
|
81
|
+
devices.command('update <id>').option('--name <name>', 'Device name').option('--nickname <nickname...>', 'Replace nicknames').option('--clear-nicknames', 'Remove all nicknames')
|
|
82
|
+
.action(async (id, opts) => {
|
|
83
|
+
if (opts.clearNicknames && opts.nickname)
|
|
84
|
+
throw new Error('Choose --nickname or --clear-nicknames.');
|
|
85
|
+
if (opts.name === undefined && opts.nickname === undefined && !opts.clearNicknames)
|
|
86
|
+
throw new Error('Supply --name, --nickname or --clear-nicknames.');
|
|
87
|
+
const result = await (await client()).beacons.devices.update(id, { ...(opts.name === undefined ? {} : { name: opts.name }), ...(opts.clearNicknames ? { nicknames: [] } : opts.nickname ? { nicknames: opts.nickname } : {}) });
|
|
88
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
89
|
+
});
|
|
90
|
+
devices.command('revoke <id>').description('Request durable mesh teardown').action(async (id) => {
|
|
91
|
+
const result = await (await client()).beacons.devices.revoke(id);
|
|
92
|
+
emit(result ?? { status: 'revoked' }, () => JSON.stringify(result ?? { status: 'revoked' }, null, 2));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SavedLogin } from './store.js';
|
|
2
|
+
/** RFC 8628 device grant against One account, for agents and machines without a local browser. */
|
|
3
|
+
export declare const DEVICE_CLIENT_ID = "genesis-cli-device";
|
|
4
|
+
export declare const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
5
|
+
export interface DeviceConfig {
|
|
6
|
+
fetch: typeof fetch;
|
|
7
|
+
sleep?: (ms: number) => Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export interface DeviceStart {
|
|
10
|
+
userCode: string;
|
|
11
|
+
verificationUri: string;
|
|
12
|
+
verificationUriComplete: string;
|
|
13
|
+
expiresIn: number;
|
|
14
|
+
interval: number;
|
|
15
|
+
/** Secret polled by this process; never printed. */
|
|
16
|
+
readonly deviceCode: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function startDeviceLogin(c: DeviceConfig, signal?: AbortSignal): Promise<DeviceStart>;
|
|
19
|
+
/** Saved session from an OAuth token response. Authority stays with the One account verifier. */
|
|
20
|
+
export declare function parseDeviceTokens(body: Record<string, unknown>, now?: number): SavedLogin;
|
|
21
|
+
/** Poll until approved, denied or expired, honouring `interval` and `slow_down`. */
|
|
22
|
+
export declare function completeDeviceLogin(c: DeviceConfig, start: DeviceStart, signal?: AbortSignal): Promise<SavedLogin>;
|
|
23
|
+
/** Rotate a device session through the OAuth refresh grant of the same public client. */
|
|
24
|
+
export declare function refreshDeviceLogin(c: DeviceConfig, saved: SavedLogin): Promise<SavedLogin>;
|
|
25
|
+
export declare function requireDeviceSaved(s: SavedLogin): void;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { LIFEID_ISSUER } from './lifeid-native.js';
|
|
2
|
+
import { errorToken } from '@onesi/genesis';
|
|
3
|
+
/** RFC 8628 device grant against One account, for agents and machines without a local browser. */
|
|
4
|
+
export const DEVICE_CLIENT_ID = 'genesis-cli-device';
|
|
5
|
+
export const DEVICE_GRANT = 'urn:ietf:params:oauth:grant-type:device_code';
|
|
6
|
+
const DEVICE_SCOPE = 'openid offline_access email profile';
|
|
7
|
+
const MAX_BODY = 32_768;
|
|
8
|
+
class DeviceLoginError extends Error {
|
|
9
|
+
}
|
|
10
|
+
function object(v) {
|
|
11
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v) ? v : undefined;
|
|
12
|
+
}
|
|
13
|
+
function secret(v) {
|
|
14
|
+
return typeof v === 'string' && v.length > 0 && v.length <= 16_384 && !/[\s\x00-\x1f\x7f]/.test(v);
|
|
15
|
+
}
|
|
16
|
+
function claimsOf(token) {
|
|
17
|
+
try {
|
|
18
|
+
const parts = token.split('.');
|
|
19
|
+
if (parts.length !== 3 || !parts.every(p => /^[A-Za-z0-9_-]+$/.test(p)))
|
|
20
|
+
return undefined;
|
|
21
|
+
return object(JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8')));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const ISSUER_HOST = new URL(LIFEID_ISSUER).host;
|
|
28
|
+
/** https on exactly the issuer's host (and port), with no userinfo or control characters. */
|
|
29
|
+
function onIssuer(u) {
|
|
30
|
+
if (typeof u !== 'string' || u.length > 2048 || /[\s\x00-\x1f\x7f-\x9f]/.test(u))
|
|
31
|
+
return false;
|
|
32
|
+
try {
|
|
33
|
+
const url = new URL(u);
|
|
34
|
+
return url.protocol === 'https:' && url.host === ISSUER_HOST && url.username === '' && url.password === '' && !/@/.test(u.split(/[/?#]/, 3)[2] ?? '');
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function invalid() { throw new DeviceLoginError('One account returned an invalid device login response. Run `genesis login --device` again.'); }
|
|
41
|
+
/** Form POST to One account: bounded body, no redirects, 15s deadline, and no response echo. */
|
|
42
|
+
async function postForm(c, path, form, signal) {
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const abort = () => controller.abort();
|
|
45
|
+
const timer = setTimeout(abort, 15_000);
|
|
46
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
47
|
+
try {
|
|
48
|
+
const response = await c.fetch(`${LIFEID_ISSUER}${path}`, {
|
|
49
|
+
method: 'POST', redirect: 'error', cache: 'no-store', signal: controller.signal,
|
|
50
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },
|
|
51
|
+
body: new URLSearchParams(form).toString(),
|
|
52
|
+
});
|
|
53
|
+
if (!/^application\/json(?:;|$)/i.test(response.headers.get('content-type') ?? '')) {
|
|
54
|
+
void response.body?.cancel().catch(() => { });
|
|
55
|
+
throw new DeviceLoginError(`One account request failed (${response.status}).`);
|
|
56
|
+
}
|
|
57
|
+
const reader = response.body?.getReader();
|
|
58
|
+
if (!reader)
|
|
59
|
+
invalid();
|
|
60
|
+
const chunks = [];
|
|
61
|
+
let size = 0;
|
|
62
|
+
for (;;) {
|
|
63
|
+
const part = await reader.read();
|
|
64
|
+
if (part.done)
|
|
65
|
+
break;
|
|
66
|
+
size += part.value.byteLength;
|
|
67
|
+
if (size > MAX_BODY) {
|
|
68
|
+
void reader.cancel().catch(() => { });
|
|
69
|
+
invalid();
|
|
70
|
+
}
|
|
71
|
+
chunks.push(part.value);
|
|
72
|
+
}
|
|
73
|
+
let parsed;
|
|
74
|
+
try {
|
|
75
|
+
parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
invalid();
|
|
79
|
+
}
|
|
80
|
+
return { status: response.status, body: object(parsed) ?? invalid() };
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error instanceof DeviceLoginError)
|
|
84
|
+
throw error;
|
|
85
|
+
if (signal?.aborted)
|
|
86
|
+
throw new DeviceLoginError('One account device login cancelled.');
|
|
87
|
+
throw new DeviceLoginError('One account transport failed. No automatic retry was made.');
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
signal?.removeEventListener('abort', abort);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export async function startDeviceLogin(c, signal) {
|
|
95
|
+
const { status, body } = await postForm(c, '/v4/oauth/device_authorization', { client_id: DEVICE_CLIENT_ID, scope: DEVICE_SCOPE }, signal);
|
|
96
|
+
if (status !== 200)
|
|
97
|
+
throw new DeviceLoginError(`One account refused the device login (${status}).`);
|
|
98
|
+
const { device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval } = body;
|
|
99
|
+
if (!secret(device_code) || typeof user_code !== 'string' || !/^[BCDFGHJKLMNPQRSTVWXZ]{4}-[BCDFGHJKLMNPQRSTVWXZ]{4}$/.test(user_code)
|
|
100
|
+
|| typeof expires_in !== 'number' || !Number.isInteger(expires_in) || expires_in < 1 || expires_in > 3600
|
|
101
|
+
|| typeof interval !== 'number' || !Number.isInteger(interval) || interval < 1 || interval > 60)
|
|
102
|
+
invalid();
|
|
103
|
+
if (!onIssuer(verification_uri) || !onIssuer(verification_uri_complete)) {
|
|
104
|
+
throw new DeviceLoginError(`One account returned a verification URL outside ${ISSUER_HOST}. Refusing to show it; run \`genesis login --device\` again.`);
|
|
105
|
+
}
|
|
106
|
+
return { deviceCode: device_code, userCode: user_code, verificationUri: verification_uri,
|
|
107
|
+
verificationUriComplete: verification_uri_complete, expiresIn: expires_in, interval };
|
|
108
|
+
}
|
|
109
|
+
/** Saved session from an OAuth token response. Authority stays with the One account verifier. */
|
|
110
|
+
export function parseDeviceTokens(body, now = Date.now() / 1000) {
|
|
111
|
+
const access = body.access_token, refresh = body.refresh_token;
|
|
112
|
+
if (body.token_type !== 'Bearer' || !secret(access) || !secret(refresh))
|
|
113
|
+
invalid();
|
|
114
|
+
const claims = claimsOf(access);
|
|
115
|
+
const exp = claims?.exp;
|
|
116
|
+
if (!claims || claims.iss !== LIFEID_ISSUER || claims.kind !== 'human' || typeof claims.sub !== 'string'
|
|
117
|
+
|| typeof claims.sid !== 'string' || typeof exp !== 'number' || exp <= now
|
|
118
|
+
|| claims.cnf !== undefined || claims.cap !== undefined || claims.obo !== undefined)
|
|
119
|
+
invalid();
|
|
120
|
+
const org = typeof claims.org === 'string' ? claims.org : undefined;
|
|
121
|
+
const email = typeof claims.email === 'string' ? claims.email : undefined;
|
|
122
|
+
return { access_token: access, refresh_token: refresh, user: { id: claims.sub, ...(email ? { email } : {}) },
|
|
123
|
+
...(org ? { organization_id: org } : {}), expires_at: exp, client_id: DEVICE_CLIENT_ID,
|
|
124
|
+
auth: { kind: 'lifeid-device', issuer: LIFEID_ISSUER, client_id: DEVICE_CLIENT_ID, principal_id: claims.sub, session_id: claims.sid } };
|
|
125
|
+
}
|
|
126
|
+
/** Poll until approved, denied or expired, honouring `interval` and `slow_down`. */
|
|
127
|
+
export async function completeDeviceLogin(c, start, signal) {
|
|
128
|
+
const sleep = c.sleep ?? ((ms) => new Promise(r => setTimeout(r, ms)));
|
|
129
|
+
const deadline = Date.now() + start.expiresIn * 1000;
|
|
130
|
+
let interval = start.interval;
|
|
131
|
+
for (;;) {
|
|
132
|
+
await sleep(interval * 1000);
|
|
133
|
+
if (signal?.aborted)
|
|
134
|
+
throw new DeviceLoginError('One account device login cancelled.');
|
|
135
|
+
if (Date.now() > deadline)
|
|
136
|
+
throw new DeviceLoginError('The device code expired. Run `genesis login --device` again.');
|
|
137
|
+
const { status, body } = await postForm(c, '/v4/oauth/token', { grant_type: DEVICE_GRANT, client_id: DEVICE_CLIENT_ID, device_code: start.deviceCode }, signal);
|
|
138
|
+
if (status === 200)
|
|
139
|
+
return parseDeviceTokens(body);
|
|
140
|
+
// RFC 8628 string errors from Auth; the envelope form (error.code) is accepted too.
|
|
141
|
+
switch (errorToken(body)) {
|
|
142
|
+
case 'authorization_pending': continue;
|
|
143
|
+
case 'slow_down':
|
|
144
|
+
interval = Math.min(interval + 5, 60);
|
|
145
|
+
continue;
|
|
146
|
+
case 'access_denied': throw new DeviceLoginError('Sign-in was denied on the verification page.');
|
|
147
|
+
case 'expired_token': throw new DeviceLoginError('The device code expired. Run `genesis login --device` again.');
|
|
148
|
+
default: throw new DeviceLoginError(`One account refused the device login (${status}). Run \`genesis login --device\` again.`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** Rotate a device session through the OAuth refresh grant of the same public client. */
|
|
153
|
+
export async function refreshDeviceLogin(c, saved) {
|
|
154
|
+
if (!secret(saved.refresh_token))
|
|
155
|
+
throw new DeviceLoginError('Session expired. Run `genesis login --device`.');
|
|
156
|
+
const { status, body } = await postForm(c, '/v4/oauth/token', { grant_type: 'refresh_token', client_id: DEVICE_CLIENT_ID, refresh_token: saved.refresh_token });
|
|
157
|
+
if (status !== 200)
|
|
158
|
+
throw new DeviceLoginError(`One account refresh failed (${status}). Run \`genesis login --device\` again.`);
|
|
159
|
+
const next = parseDeviceTokens(body);
|
|
160
|
+
if (next.auth?.principal_id !== saved.auth?.principal_id)
|
|
161
|
+
invalid();
|
|
162
|
+
return next;
|
|
163
|
+
}
|
|
164
|
+
export function requireDeviceSaved(s) {
|
|
165
|
+
const a = s.auth;
|
|
166
|
+
const claims = claimsOf(s.access_token);
|
|
167
|
+
if (!a || a.kind !== 'lifeid-device' || a.issuer !== LIFEID_ISSUER || a.client_id !== DEVICE_CLIENT_ID || s.client_id !== DEVICE_CLIENT_ID
|
|
168
|
+
|| !claims || claims.iss !== a.issuer || claims.kind !== 'human' || claims.sub !== a.principal_id || claims.sid !== a.session_id
|
|
169
|
+
|| (s.organization_id !== undefined && claims.org !== s.organization_id)
|
|
170
|
+
|| claims.cnf !== undefined || claims.cap !== undefined || claims.obo !== undefined) {
|
|
171
|
+
throw new Error('Saved One account device session is inconsistent. Run `genesis login --device` again.');
|
|
172
|
+
}
|
|
173
|
+
}
|
package/dist/dials.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import type { Genesis } from '@onesi/genesis';
|
|
3
|
+
export declare function registerDials(program: Command, dependencies: {
|
|
4
|
+
client: () => Promise<Genesis>;
|
|
5
|
+
orgId: (client: Genesis) => Promise<string>;
|
|
6
|
+
emit: (data: unknown, pretty: () => string) => void;
|
|
7
|
+
}): void;
|