@alvera-ai/platform-sdk 0.7.3 → 0.9.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 +25 -19
- package/package.json +3 -4
- package/src/cli/auth.ts +174 -0
- package/src/cli/env.ts +49 -0
- package/src/cli/helpers.ts +128 -0
- package/src/cli/init.ts +123 -0
- package/src/cli/raw.ts +65 -0
- package/src/cli/resources.ts +789 -0
- package/src/cli/workflows.ts +246 -0
- package/src/cli.ts +15 -1347
- package/src/client.ts +30 -8
- package/src/config.ts +4 -0
- package/src/generated/sdk.gen.ts +932 -932
- package/src/generated/types.gen.ts +7220 -7220
- package/src/generated/valibot.gen.ts +4349 -4341
- package/src/index.ts +1 -0
package/README.md
CHANGED
|
@@ -10,6 +10,8 @@ tables, action status updaters, AI agents — with full type safety.
|
|
|
10
10
|
```bash
|
|
11
11
|
npm install @alvera-ai/platform-sdk
|
|
12
12
|
# or
|
|
13
|
+
bun add @alvera-ai/platform-sdk
|
|
14
|
+
# or
|
|
13
15
|
pnpm add @alvera-ai/platform-sdk
|
|
14
16
|
```
|
|
15
17
|
|
|
@@ -141,25 +143,30 @@ The CLI resolves a base URL with this precedence (highest first):
|
|
|
141
143
|
|
|
142
144
|
Adding, renaming, or removing environments happens in the platform repo (the
|
|
143
145
|
`servers/0` function in `lib/platform_api/api_spec.ex`); rerun
|
|
144
|
-
`
|
|
146
|
+
`bun run regen` in the SDK to pick up the change.
|
|
145
147
|
|
|
146
148
|
## Resources
|
|
147
149
|
|
|
148
|
-
| Resource
|
|
149
|
-
|
|
150
|
-
| `ping`
|
|
151
|
-
| `sessions`
|
|
152
|
-
| `
|
|
153
|
-
| `
|
|
154
|
-
| `
|
|
155
|
-
| `
|
|
156
|
-
| `
|
|
157
|
-
| `
|
|
158
|
-
| `
|
|
159
|
-
| `
|
|
160
|
-
| `
|
|
161
|
-
| `
|
|
162
|
-
| `
|
|
150
|
+
| Resource | Operations |
|
|
151
|
+
|------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
|
|
152
|
+
| `ping` | health check |
|
|
153
|
+
| `sessions` | `verify` |
|
|
154
|
+
| `auth` | `signUp` |
|
|
155
|
+
| `admin` | `confirmUser` |
|
|
156
|
+
| `tenants` | `list`, `create` |
|
|
157
|
+
| `invitations` | `list`, `create`, `accept` |
|
|
158
|
+
| `datasets` | `search`, `metadata`, `createUserSearch` |
|
|
159
|
+
| `datalakes` | `list`, `get`, `create`, `metadata`, `migrate`, `createUploadLink`, `createDownloadLink` |
|
|
160
|
+
| `dataSources` | `list`, `create`, `update` |
|
|
161
|
+
| `tools` | `list`, `get`, `create`, `update`, `delete`, `testInvocation` |
|
|
162
|
+
| `genericTables` | `list`, `create` |
|
|
163
|
+
| `actionStatusUpdaters` | `list`, `create`, `update` |
|
|
164
|
+
| `aiAgents` | `list`, `get`, `create`, `update`, `delete` |
|
|
165
|
+
| `connectedApps` | `list`, `get`, `create`, `update`, `syncRoutes`, `resolvePage`, `updateMessageTracking` |
|
|
166
|
+
| `dataActivationClients` | `list`, `get`, `create`, `update`, `delete`, `metadata`, `runManually`, `ingest`, `ingestFile`, `logs.list`, `logs.get` |
|
|
167
|
+
| `interoperabilityContracts` | `list`, `get`, `create`, `update`, `delete`, `metadata`, `run` |
|
|
168
|
+
| `mdm` | `verify` |
|
|
169
|
+
| `workflows` | `list`, `get`, `create`, `update`, `delete`, `metadata`, `execute`, `run`, `workflowLogs.list/get/download`, `batchLogs.list/get/start/stop/refresh` |
|
|
163
170
|
|
|
164
171
|
Tenant and datalake provisioning are performed by Alvera admins — contact your
|
|
165
172
|
representative to onboard a new tenant.
|
|
@@ -290,12 +297,11 @@ git commit -am "feat(api): …"
|
|
|
290
297
|
|
|
291
298
|
# in this repo
|
|
292
299
|
cp ../platform/openapi.yaml spec/openapi.yaml
|
|
293
|
-
|
|
294
|
-
pnpm build # compile to dist/
|
|
300
|
+
bun run regen # gen-environments + codegen + check-coverage
|
|
295
301
|
git commit -am "chore: sync openapi spec"
|
|
296
302
|
```
|
|
297
303
|
|
|
298
|
-
`
|
|
304
|
+
`bun run regen` runs `gen-environments` (rebuilds
|
|
299
305
|
`src/environments.generated.ts` from `servers[]`), then `codegen`, then
|
|
300
306
|
`check-coverage`.
|
|
301
307
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alvera-ai/platform-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Typed SDK for the Alvera platform API — manage data sources, tools, generic tables, AI agents, and action status updaters.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -24,15 +24,14 @@
|
|
|
24
24
|
"engines": {
|
|
25
25
|
"node": ">=20"
|
|
26
26
|
},
|
|
27
|
-
"packageManager": "pnpm@10.20.0",
|
|
28
27
|
"scripts": {
|
|
29
28
|
"gen-environments": "tsx scripts/gen-environments.ts",
|
|
30
29
|
"codegen": "openapi-ts && tsx scripts/patch-generated.ts",
|
|
31
30
|
"check-coverage": "tsx scripts/check-sdk-coverage.ts",
|
|
32
|
-
"regen": "
|
|
31
|
+
"regen": "bun run gen-environments && bun run codegen && bun run check-coverage",
|
|
33
32
|
"typecheck": "tsc --noEmit && tsc --noEmit -p scripts/tsconfig.json",
|
|
34
33
|
"clean": "rm -rf src/generated src/environments.generated.ts",
|
|
35
|
-
"prepare": "
|
|
34
|
+
"prepare": "bun run gen-environments && bun run codegen && bun run check-coverage"
|
|
36
35
|
},
|
|
37
36
|
"dependencies": {
|
|
38
37
|
"commander": "14.0.3",
|
package/src/cli/auth.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { createSession, createUnvalidatedPlatformApi, revokeSession } from '../client.js';
|
|
3
|
+
import {
|
|
4
|
+
CONFIG_PATHS,
|
|
5
|
+
ENVIRONMENTS,
|
|
6
|
+
clearProfileCreds,
|
|
7
|
+
getProfileName,
|
|
8
|
+
readProfileConfig,
|
|
9
|
+
resolveProfile,
|
|
10
|
+
writeProfileConfig,
|
|
11
|
+
writeProfileCreds,
|
|
12
|
+
} from '../config.js';
|
|
13
|
+
import { type GlobalOpts, authedApi, die, out, prompt, run } from './helpers.js';
|
|
14
|
+
|
|
15
|
+
const ENVIRONMENT_NAMES = Object.keys(ENVIRONMENTS);
|
|
16
|
+
|
|
17
|
+
export function register(program: Command): void {
|
|
18
|
+
program
|
|
19
|
+
.command('configure')
|
|
20
|
+
.description('Interactively set defaults (environment, tenant, email) for a profile')
|
|
21
|
+
.action(async () => {
|
|
22
|
+
const profile = getProfileName(program.opts<GlobalOpts>().profile);
|
|
23
|
+
const current = resolveProfile(profile);
|
|
24
|
+
const currentCfg = readProfileConfig(profile);
|
|
25
|
+
|
|
26
|
+
const options = ENVIRONMENT_NAMES.map(
|
|
27
|
+
(name) => `${name} (${ENVIRONMENTS[name as keyof typeof ENVIRONMENTS].base_url})`,
|
|
28
|
+
).join(', ');
|
|
29
|
+
const defaultChoice = currentCfg.environment ?? current.environment;
|
|
30
|
+
const envInput =
|
|
31
|
+
(await prompt(`Environment [${defaultChoice}] — one of: ${options}, or a custom URL: `)) ||
|
|
32
|
+
defaultChoice;
|
|
33
|
+
|
|
34
|
+
const patch: { environment?: string; base_url?: string; tenant_slug?: string; email?: string } = {};
|
|
35
|
+
const unset: Array<'environment' | 'base_url'> = [];
|
|
36
|
+
if (ENVIRONMENT_NAMES.includes(envInput)) {
|
|
37
|
+
patch.environment = envInput;
|
|
38
|
+
unset.push('base_url');
|
|
39
|
+
} else if (/^https?:\/\//.test(envInput)) {
|
|
40
|
+
patch.base_url = envInput;
|
|
41
|
+
unset.push('environment');
|
|
42
|
+
} else {
|
|
43
|
+
die(
|
|
44
|
+
`"${envInput}" is not a known environment or a URL. ` +
|
|
45
|
+
`Valid: ${ENVIRONMENT_NAMES.join(', ')} or http(s)://…`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
patch.tenant_slug =
|
|
50
|
+
(await prompt(`Default tenant slug [${current.tenantSlug ?? ''}]: `)) || current.tenantSlug || '';
|
|
51
|
+
patch.email = (await prompt(`Email [${current.email ?? ''}]: `)) || current.email || '';
|
|
52
|
+
writeProfileConfig(profile, patch, unset);
|
|
53
|
+
process.stderr.write(`Saved profile "${profile}" → ${CONFIG_PATHS.config}\n`);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
program
|
|
57
|
+
.command('login')
|
|
58
|
+
.description('Exchange credentials for a session token and store it')
|
|
59
|
+
.option('--email <email>')
|
|
60
|
+
.option('--password <password>')
|
|
61
|
+
.option('--tenant <slug>')
|
|
62
|
+
.option('--base-url <url>')
|
|
63
|
+
.option('--expires-in <seconds>', 'session duration (default 86400, max 2592000)')
|
|
64
|
+
.action(async (opts: Record<string, string>) => {
|
|
65
|
+
const profile = getProfileName(program.opts<GlobalOpts>().profile);
|
|
66
|
+
const current = resolveProfile(profile);
|
|
67
|
+
const baseUrl = opts.baseUrl ?? current.baseUrl;
|
|
68
|
+
const email = opts.email ?? current.email ?? (await prompt('Email: '));
|
|
69
|
+
const password = opts.password ?? process.env.ALVERA_PASSWORD ?? (await prompt('Password: ', { hidden: true }));
|
|
70
|
+
const tenant = opts.tenant ?? current.tenantSlug ?? (await prompt('Tenant slug: '));
|
|
71
|
+
if (!email || !password || !tenant) die('email, password, and tenant are required');
|
|
72
|
+
|
|
73
|
+
await run(async () => {
|
|
74
|
+
const session = await createSession({
|
|
75
|
+
baseUrl,
|
|
76
|
+
email,
|
|
77
|
+
password,
|
|
78
|
+
tenantSlug: tenant,
|
|
79
|
+
expiresIn: opts.expiresIn ? Number(opts.expiresIn) : undefined,
|
|
80
|
+
});
|
|
81
|
+
writeProfileConfig(profile, {
|
|
82
|
+
...(opts.baseUrl ? { base_url: baseUrl } : {}),
|
|
83
|
+
tenant_slug: tenant,
|
|
84
|
+
email,
|
|
85
|
+
});
|
|
86
|
+
clearProfileCreds(profile);
|
|
87
|
+
writeProfileCreds(profile, {
|
|
88
|
+
session_token: session.sessionToken,
|
|
89
|
+
expires_at: session.expiresAt ?? '',
|
|
90
|
+
});
|
|
91
|
+
const tenantLabel = session.tenant ? `tenant "${session.tenant.slug}"` : 'no tenant';
|
|
92
|
+
process.stderr.write(
|
|
93
|
+
`Logged in as ${email} → ${tenantLabel} (profile "${profile}").\n` +
|
|
94
|
+
`Token stored in ${CONFIG_PATHS.credentials}\n` +
|
|
95
|
+
(session.expiresAt ? `Expires at ${session.expiresAt}\n` : ''),
|
|
96
|
+
);
|
|
97
|
+
return undefined;
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
program
|
|
102
|
+
.command('logout')
|
|
103
|
+
.description('Revoke the current session and clear stored credentials')
|
|
104
|
+
.action(async () => {
|
|
105
|
+
const profile = getProfileName(program.opts<GlobalOpts>().profile);
|
|
106
|
+
const resolved = resolveProfile(profile);
|
|
107
|
+
if (resolved.sessionToken) {
|
|
108
|
+
createUnvalidatedPlatformApi({ baseUrl: resolved.baseUrl, sessionToken: resolved.sessionToken });
|
|
109
|
+
try {
|
|
110
|
+
await revokeSession();
|
|
111
|
+
} catch {
|
|
112
|
+
// Token may already be invalid/expired — clear local state anyway.
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
clearProfileCreds(profile);
|
|
116
|
+
process.stderr.write(`Cleared credentials for profile "${profile}".\n`);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
program
|
|
120
|
+
.command('whoami')
|
|
121
|
+
.description('Print the current profile configuration')
|
|
122
|
+
.action(() => {
|
|
123
|
+
const profile = getProfileName(program.opts<GlobalOpts>().profile);
|
|
124
|
+
const resolved = resolveProfile(profile);
|
|
125
|
+
out({
|
|
126
|
+
profile: resolved.profile,
|
|
127
|
+
environment: resolved.environment,
|
|
128
|
+
baseUrl: resolved.baseUrl,
|
|
129
|
+
tenantSlug: resolved.tenantSlug,
|
|
130
|
+
email: resolved.email,
|
|
131
|
+
hasSessionToken: Boolean(resolved.sessionToken),
|
|
132
|
+
expiresAt: resolved.expiresAt,
|
|
133
|
+
hasApiKey: Boolean(resolved.apiKey),
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
program
|
|
138
|
+
.command('set-api-key')
|
|
139
|
+
.description('Store an API key for the current profile (used instead of session token)')
|
|
140
|
+
.argument('[key]', 'API key (omit to read from prompt or ALVERA_API_KEY)')
|
|
141
|
+
.action(async (key?: string) => {
|
|
142
|
+
const profile = getProfileName(program.opts<GlobalOpts>().profile);
|
|
143
|
+
const apiKey = key ?? process.env.ALVERA_API_KEY ?? (await prompt('API key: ', { hidden: true }));
|
|
144
|
+
if (!apiKey) die('API key is required');
|
|
145
|
+
clearProfileCreds(profile);
|
|
146
|
+
writeProfileCreds(profile, { api_key: apiKey });
|
|
147
|
+
process.stderr.write(
|
|
148
|
+
`API key stored for profile "${profile}" → ${CONFIG_PATHS.credentials}\n` +
|
|
149
|
+
`(session token cleared)\n`,
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
program
|
|
154
|
+
.command('ping')
|
|
155
|
+
.description('Health check')
|
|
156
|
+
.action(async () => {
|
|
157
|
+
await run(async () => {
|
|
158
|
+
const { api } = authedApi(program.opts<GlobalOpts>());
|
|
159
|
+
const { data } = await api.ping();
|
|
160
|
+
return data;
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
program
|
|
165
|
+
.command('sessions-verify')
|
|
166
|
+
.description('Verify the current session token via the API')
|
|
167
|
+
.action(async () => {
|
|
168
|
+
await run(async () => {
|
|
169
|
+
const { api } = authedApi(program.opts<GlobalOpts>());
|
|
170
|
+
const { data } = await api.sessions.verify();
|
|
171
|
+
return data;
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
package/src/cli/env.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_ENVIRONMENT,
|
|
4
|
+
ENVIRONMENTS,
|
|
5
|
+
getProfileName,
|
|
6
|
+
resolveProfile,
|
|
7
|
+
writeProfileConfig,
|
|
8
|
+
} from '../config.js';
|
|
9
|
+
import { type GlobalOpts, die, out } from './helpers.js';
|
|
10
|
+
|
|
11
|
+
const ENVIRONMENT_NAMES = Object.keys(ENVIRONMENTS);
|
|
12
|
+
|
|
13
|
+
export function register(program: Command): void {
|
|
14
|
+
const envCmd = program
|
|
15
|
+
.command('env')
|
|
16
|
+
.description('List and switch Alvera API environments');
|
|
17
|
+
|
|
18
|
+
envCmd
|
|
19
|
+
.command('list')
|
|
20
|
+
.description('List available environments (from spec/openapi.yaml)')
|
|
21
|
+
.action(() => {
|
|
22
|
+
const profile = getProfileName(program.opts<GlobalOpts>().profile);
|
|
23
|
+
const resolved = resolveProfile(profile);
|
|
24
|
+
out(
|
|
25
|
+
ENVIRONMENT_NAMES.map((name) => ({
|
|
26
|
+
name,
|
|
27
|
+
baseUrl: ENVIRONMENTS[name as keyof typeof ENVIRONMENTS].base_url,
|
|
28
|
+
description: ENVIRONMENTS[name as keyof typeof ENVIRONMENTS].description,
|
|
29
|
+
default: name === DEFAULT_ENVIRONMENT,
|
|
30
|
+
active: name === resolved.environment,
|
|
31
|
+
})),
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
envCmd
|
|
36
|
+
.command('use <name>')
|
|
37
|
+
.description('Persist the selected environment to the profile (clears any custom base_url)')
|
|
38
|
+
.action((name: string) => {
|
|
39
|
+
if (!ENVIRONMENT_NAMES.includes(name)) {
|
|
40
|
+
die(`unknown environment "${name}". Valid: ${ENVIRONMENT_NAMES.join(', ')}`);
|
|
41
|
+
}
|
|
42
|
+
const profile = getProfileName(program.opts<GlobalOpts>().profile);
|
|
43
|
+
writeProfileConfig(profile, { environment: name }, ['base_url']);
|
|
44
|
+
process.stderr.write(
|
|
45
|
+
`Profile "${profile}" now uses environment "${name}" ` +
|
|
46
|
+
`(${ENVIRONMENTS[name as keyof typeof ENVIRONMENTS].base_url}).\n`,
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { ValiError } from 'valibot';
|
|
5
|
+
import { createUnvalidatedPlatformApi } from '../client.js';
|
|
6
|
+
import { getProfileName, resolveProfile } from '../config.js';
|
|
7
|
+
|
|
8
|
+
export interface GlobalOpts {
|
|
9
|
+
profile?: string;
|
|
10
|
+
env?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function out(value: unknown): void {
|
|
14
|
+
process.stdout.write(JSON.stringify(value, null, 2) + '\n');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function die(message: string, code = 1): never {
|
|
18
|
+
process.stderr.write(`alvera: ${message}\n`);
|
|
19
|
+
process.exit(code);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function prompt(question: string, { hidden = false } = {}): Promise<string> {
|
|
23
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
|
|
24
|
+
if (!hidden) {
|
|
25
|
+
return new Promise((resolve) => rl.question(question, (ans) => {
|
|
26
|
+
rl.close();
|
|
27
|
+
resolve(ans);
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
const anyRl = rl as unknown as { _writeToOutput: (s: string) => void };
|
|
31
|
+
const originalWrite = anyRl._writeToOutput.bind(rl);
|
|
32
|
+
anyRl._writeToOutput = (s: string) => {
|
|
33
|
+
if (s.includes(question)) originalWrite(s);
|
|
34
|
+
else originalWrite('');
|
|
35
|
+
};
|
|
36
|
+
return new Promise((resolve) => rl.question(question, (ans) => {
|
|
37
|
+
rl.close();
|
|
38
|
+
process.stderr.write('\n');
|
|
39
|
+
resolve(ans);
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function readBody(body: string | undefined, bodyFile: string | undefined): Record<string, unknown> {
|
|
44
|
+
if (body && bodyFile) die('use only one of --body or --body-file');
|
|
45
|
+
let raw: string;
|
|
46
|
+
if (body) raw = body;
|
|
47
|
+
else if (bodyFile === '-') raw = readFileSync(0, 'utf8');
|
|
48
|
+
else if (bodyFile) raw = readFileSync(bodyFile, 'utf8');
|
|
49
|
+
else die('missing request body (pass --body <json> or --body-file <path>)');
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
53
|
+
die('body must be a JSON object');
|
|
54
|
+
}
|
|
55
|
+
return parsed;
|
|
56
|
+
} catch (err) {
|
|
57
|
+
die(`invalid JSON in body: ${(err as Error).message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveTenant(explicit: string | undefined, profileTenant: string | null): string {
|
|
62
|
+
const tenant = explicit ?? profileTenant;
|
|
63
|
+
if (!tenant) die('tenant slug required (pass as argument or set `tenant_slug` in the profile)');
|
|
64
|
+
return tenant;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function authedApi(opts: GlobalOpts) {
|
|
68
|
+
const profile = getProfileName(opts.profile);
|
|
69
|
+
const resolved = resolveProfile(profile);
|
|
70
|
+
|
|
71
|
+
if (resolved.apiKey) {
|
|
72
|
+
return {
|
|
73
|
+
api: createUnvalidatedPlatformApi({ baseUrl: resolved.baseUrl, apiKey: resolved.apiKey }),
|
|
74
|
+
resolved,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!resolved.sessionToken) {
|
|
79
|
+
die(
|
|
80
|
+
`no credentials for profile "${profile}". ` +
|
|
81
|
+
`Run \`alvera login --profile ${profile}\`, set ALVERA_SESSION_TOKEN, or set ALVERA_API_KEY.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (resolved.expiresAt && new Date(resolved.expiresAt) < new Date()) {
|
|
85
|
+
die(
|
|
86
|
+
`session for profile "${profile}" expired at ${resolved.expiresAt}. ` +
|
|
87
|
+
`Run \`alvera login --profile ${profile}\` to refresh.`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
api: createUnvalidatedPlatformApi({ baseUrl: resolved.baseUrl, sessionToken: resolved.sessionToken }),
|
|
92
|
+
resolved,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function run(fn: () => Promise<unknown>): Promise<void> {
|
|
97
|
+
try {
|
|
98
|
+
const result = await fn();
|
|
99
|
+
if (result !== undefined) out(result);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
die(formatError(err));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function formatError(err: unknown): string {
|
|
106
|
+
if (err instanceof ValiError) {
|
|
107
|
+
const lines = err.issues.map((issue) => {
|
|
108
|
+
const path = (issue.path ?? [])
|
|
109
|
+
.map((p: { key?: unknown }) => String(p.key ?? '?'))
|
|
110
|
+
.join('.') || '(root)';
|
|
111
|
+
return ` ${path}: ${issue.message}`;
|
|
112
|
+
});
|
|
113
|
+
return `validation failed:\n${lines.join('\n')}`;
|
|
114
|
+
}
|
|
115
|
+
if (err instanceof Error) return err.message;
|
|
116
|
+
if (err && typeof err === 'object') {
|
|
117
|
+
const detail = (err as { errors?: { detail?: unknown } }).errors?.detail;
|
|
118
|
+
if (typeof detail === 'string') return detail;
|
|
119
|
+
return JSON.stringify(err, null, 2);
|
|
120
|
+
}
|
|
121
|
+
return String(err);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function bodyOption(cmd: Command): Command {
|
|
125
|
+
return cmd
|
|
126
|
+
.option('--body <json>', 'request body as a JSON string')
|
|
127
|
+
.option('--body-file <path>', 'path to a JSON file (or "-" for stdin)');
|
|
128
|
+
}
|
package/src/cli/init.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
|
|
6
|
+
interface InitField {
|
|
7
|
+
key: string;
|
|
8
|
+
label: string;
|
|
9
|
+
default?: string;
|
|
10
|
+
}
|
|
11
|
+
type InitSection = { header: string; fields: InitField[] }
|
|
12
|
+
|
|
13
|
+
const DB_KEYS = ['HOST', 'PORT', 'NAME', 'SCHEMA', 'AUTH_METHOD', 'USER', 'PASS', 'ENABLE_SSL'] as const;
|
|
14
|
+
const DB_LABELS = ['host', 'port', 'database name', 'schema', 'auth method', 'user', 'password', 'enable SSL'] as const;
|
|
15
|
+
const DB_DEFAULTS: Record<string, string> = { PORT: '5432', SCHEMA: 'public', AUTH_METHOD: 'password', ENABLE_SSL: 'false' };
|
|
16
|
+
|
|
17
|
+
function dbSection(label: string, prefix: string): InitSection {
|
|
18
|
+
return {
|
|
19
|
+
header: label,
|
|
20
|
+
fields: DB_KEYS.map((k, i) => ({
|
|
21
|
+
key: `${prefix}_${k}`,
|
|
22
|
+
label: `${label} — ${DB_LABELS[i]}`,
|
|
23
|
+
default: DB_DEFAULTS[k],
|
|
24
|
+
})),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const STORAGE_KEYS = ['TYPE', 'REGION', 'ACCESS_KEY_ID', 'SECRET_ACCESS_KEY', 'ENDPOINT', 'BUCKET'] as const;
|
|
29
|
+
const STORAGE_LABELS = ['type (aws/r2)', 'region', 'access key ID', 'secret access key', 'endpoint', 'bucket'] as const;
|
|
30
|
+
|
|
31
|
+
function storageSection(label: string, prefix: string): InitSection {
|
|
32
|
+
return {
|
|
33
|
+
header: label,
|
|
34
|
+
fields: STORAGE_KEYS.map((k, i) => ({
|
|
35
|
+
key: `${prefix}_${k}`,
|
|
36
|
+
label: `${label} — ${STORAGE_LABELS[i]}`,
|
|
37
|
+
default: k === 'TYPE' ? 'aws' : undefined,
|
|
38
|
+
})),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function collectAndWrite(sections: InitSection[], outPath: string): Promise<void> {
|
|
43
|
+
const allFields = sections.flatMap((s) => s.fields);
|
|
44
|
+
|
|
45
|
+
let answers: string[];
|
|
46
|
+
if (process.stdin.isTTY) {
|
|
47
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
|
|
48
|
+
const ask = (q: string): Promise<string> =>
|
|
49
|
+
new Promise((res) => rl.question(q, (ans) => res(ans)));
|
|
50
|
+
answers = [];
|
|
51
|
+
for (const field of allFields) {
|
|
52
|
+
const dflt = field.default ?? '';
|
|
53
|
+
const suffix = dflt ? ` [${dflt}]` : '';
|
|
54
|
+
answers.push(await ask(`${field.label}${suffix}: `));
|
|
55
|
+
}
|
|
56
|
+
rl.close();
|
|
57
|
+
} else {
|
|
58
|
+
const raw = readFileSync(0, 'utf8');
|
|
59
|
+
answers = raw.split('\n');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const lines: string[] = [];
|
|
63
|
+
let idx = 0;
|
|
64
|
+
for (const section of sections) {
|
|
65
|
+
if (lines.length > 0) lines.push('');
|
|
66
|
+
lines.push(`# ${section.header}`);
|
|
67
|
+
for (const field of section.fields) {
|
|
68
|
+
const dflt = field.default ?? '';
|
|
69
|
+
const answer = (answers[idx++] ?? '').trim();
|
|
70
|
+
lines.push(`${field.key}=${answer || dflt}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const abs = resolve(process.cwd(), outPath);
|
|
75
|
+
writeFileSync(abs, lines.join('\n') + '\n');
|
|
76
|
+
process.stderr.write(`Wrote ${abs}\n`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function register(program: Command): void {
|
|
80
|
+
const initCmd = program.command('init').description('Generate a .env configuration file');
|
|
81
|
+
|
|
82
|
+
initCmd
|
|
83
|
+
.command('connected-app')
|
|
84
|
+
.description('Generate .env for SDK / connected-app integration')
|
|
85
|
+
.option('-o, --output <path>', 'output file path', '.env')
|
|
86
|
+
.action(async (opts: { output: string }) => {
|
|
87
|
+
await collectAndWrite([
|
|
88
|
+
{
|
|
89
|
+
header: 'Alvera connected-app configuration',
|
|
90
|
+
fields: [
|
|
91
|
+
{ key: 'ALVERA_BASE_URL', label: 'Base URL', default: 'https://api.alvera.ai' },
|
|
92
|
+
{ key: 'ALVERA_TENANT', label: 'Tenant slug' },
|
|
93
|
+
{ key: 'ALVERA_DATALAKE', label: 'Datalake slug' },
|
|
94
|
+
{ key: 'ALVERA_CONNECTED_APP', label: 'Connected app slug' },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
], opts.output);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
initCmd
|
|
101
|
+
.command('infra-setup')
|
|
102
|
+
.description('Generate .env for datalake infrastructure (databases + object storage)')
|
|
103
|
+
.option('-o, --output <path>', 'output file path', '.env')
|
|
104
|
+
.action(async (opts: { output: string }) => {
|
|
105
|
+
await collectAndWrite([
|
|
106
|
+
{
|
|
107
|
+
header: 'Datalake',
|
|
108
|
+
fields: [
|
|
109
|
+
{ key: 'ALVERA_DATALAKE_NAME', label: 'Datalake name' },
|
|
110
|
+
{ key: 'ALVERA_DATALAKE_DATA_DOMAIN', label: 'Data domain' },
|
|
111
|
+
{ key: 'ALVERA_DATALAKE_TIMEZONE', label: 'Timezone', default: 'UTC' },
|
|
112
|
+
{ key: 'ALVERA_DATALAKE_POOL_SIZE', label: 'Pool size', default: '10' },
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
dbSection('Database — Unregulated Reader', 'ALVERA_DB_UNREG_READER'),
|
|
116
|
+
dbSection('Database — Unregulated Writer', 'ALVERA_DB_UNREG_WRITER'),
|
|
117
|
+
dbSection('Database — Regulated Reader', 'ALVERA_DB_REG_READER'),
|
|
118
|
+
dbSection('Database — Regulated Writer', 'ALVERA_DB_REG_WRITER'),
|
|
119
|
+
storageSection('Storage — Unregulated', 'ALVERA_STORAGE_UNREG'),
|
|
120
|
+
storageSection('Storage — Regulated', 'ALVERA_STORAGE_REG'),
|
|
121
|
+
], opts.output);
|
|
122
|
+
});
|
|
123
|
+
}
|
package/src/cli/raw.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { getProfileName, resolveProfile } from '../config.js';
|
|
3
|
+
import { type GlobalOpts, bodyOption, die, out, readBody } from './helpers.js';
|
|
4
|
+
|
|
5
|
+
const ALLOWED_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] as const;
|
|
6
|
+
|
|
7
|
+
export function register(program: Command): void {
|
|
8
|
+
bodyOption(
|
|
9
|
+
program
|
|
10
|
+
.command('raw <method> <path>')
|
|
11
|
+
.description('Send an authenticated HTTP request bypassing SDK validation')
|
|
12
|
+
.option('--no-parse', 'print raw response text instead of pretty-printing JSON'),
|
|
13
|
+
)
|
|
14
|
+
.action(
|
|
15
|
+
async (
|
|
16
|
+
method: string,
|
|
17
|
+
path: string,
|
|
18
|
+
opts: { body?: string; bodyFile?: string; parse?: boolean },
|
|
19
|
+
) => {
|
|
20
|
+
const upper = method.toUpperCase();
|
|
21
|
+
if (!ALLOWED_METHODS.includes(upper as (typeof ALLOWED_METHODS)[number])) {
|
|
22
|
+
die(`invalid method "${method}". Allowed: ${ALLOWED_METHODS.join(', ')}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const globalOpts = program.opts<GlobalOpts>();
|
|
26
|
+
const profile = getProfileName(globalOpts.profile);
|
|
27
|
+
const resolved = resolveProfile(profile);
|
|
28
|
+
if (!resolved.sessionToken && !resolved.apiKey) {
|
|
29
|
+
die(
|
|
30
|
+
`no credentials for profile "${profile}". ` +
|
|
31
|
+
`Run \`alvera login\`, set ALVERA_SESSION_TOKEN, or set ALVERA_API_KEY.`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const url = `${resolved.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
|
|
36
|
+
const headers: Record<string, string> = resolved.apiKey
|
|
37
|
+
? { 'X-API-Key': resolved.apiKey }
|
|
38
|
+
: { Authorization: `Bearer ${resolved.sessionToken!}` };
|
|
39
|
+
|
|
40
|
+
let fetchBody: string | undefined;
|
|
41
|
+
if (opts.body || opts.bodyFile) {
|
|
42
|
+
const parsed = readBody(opts.body, opts.bodyFile);
|
|
43
|
+
fetchBody = JSON.stringify(parsed);
|
|
44
|
+
headers['Content-Type'] = 'application/json';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const resp = await fetch(url, { method: upper, headers, body: fetchBody });
|
|
48
|
+
const text = await resp.text();
|
|
49
|
+
|
|
50
|
+
if (!resp.ok) {
|
|
51
|
+
die(`${upper} ${path} → ${resp.status} ${resp.statusText}: ${text}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (opts.parse === false) {
|
|
55
|
+
process.stdout.write(text + '\n');
|
|
56
|
+
} else {
|
|
57
|
+
try {
|
|
58
|
+
out(JSON.parse(text));
|
|
59
|
+
} catch {
|
|
60
|
+
process.stdout.write(text + '\n');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
}
|