@north-light/sdk 0.0.0 → 0.3.415
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/LICENSE +18 -0
- package/README.md +187 -2
- package/dist/client.d.ts +176 -0
- package/dist/client.js +582 -0
- package/dist/crtr.d.ts +24 -0
- package/dist/crtr.js +50 -0
- package/dist/env-command.d.ts +15 -0
- package/dist/env-command.js +138 -0
- package/dist/error-codes.d.ts +7 -0
- package/dist/error-codes.js +43 -0
- package/dist/errors.d.ts +72 -0
- package/dist/errors.js +101 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +17 -0
- package/dist/keygen-cli.d.ts +2 -0
- package/dist/keygen-cli.js +11 -0
- package/dist/keygen-command.d.ts +6 -0
- package/dist/keygen-command.js +131 -0
- package/dist/oauth/index.d.ts +235 -0
- package/dist/oauth/index.js +621 -0
- package/dist/oauth/keygen.d.ts +18 -0
- package/dist/oauth/keygen.js +46 -0
- package/dist/oauth/refusal.d.ts +32 -0
- package/dist/oauth/refusal.js +17 -0
- package/dist/resources/activity.d.ts +22 -0
- package/dist/resources/activity.js +43 -0
- package/dist/resources/attachments.d.ts +15 -0
- package/dist/resources/attachments.js +13 -0
- package/dist/resources/bash.d.ts +9 -0
- package/dist/resources/bash.js +15 -0
- package/dist/resources/canvas/history.d.ts +11 -0
- package/dist/resources/canvas/history.js +20 -0
- package/dist/resources/canvas.d.ts +19 -0
- package/dist/resources/canvas.js +42 -0
- package/dist/resources/crons.d.ts +15 -0
- package/dist/resources/crons.js +33 -0
- package/dist/resources/custom-objects.d.ts +20 -0
- package/dist/resources/custom-objects.js +84 -0
- package/dist/resources/files.d.ts +34 -0
- package/dist/resources/files.js +34 -0
- package/dist/resources/follower.d.ts +71 -0
- package/dist/resources/follower.js +202 -0
- package/dist/resources/forward.d.ts +7 -0
- package/dist/resources/forward.js +67 -0
- package/dist/resources/grant-watcher.d.ts +57 -0
- package/dist/resources/grant-watcher.js +100 -0
- package/dist/resources/human/inbox.d.ts +14 -0
- package/dist/resources/human/inbox.js +30 -0
- package/dist/resources/human/requests.d.ts +13 -0
- package/dist/resources/human/requests.js +26 -0
- package/dist/resources/human.d.ts +8 -0
- package/dist/resources/human.js +10 -0
- package/dist/resources/identifiers.d.ts +8 -0
- package/dist/resources/identifiers.js +33 -0
- package/dist/resources/memory.d.ts +69 -0
- package/dist/resources/memory.js +30 -0
- package/dist/resources/models/config.d.ts +8 -0
- package/dist/resources/models/config.js +9 -0
- package/dist/resources/models/credentials.d.ts +10 -0
- package/dist/resources/models/credentials.js +17 -0
- package/dist/resources/models.d.ts +8 -0
- package/dist/resources/models.js +10 -0
- package/dist/resources/node-stream.d.ts +34 -0
- package/dist/resources/node-stream.js +176 -0
- package/dist/resources/nodes/jobs.d.ts +9 -0
- package/dist/resources/nodes/jobs.js +14 -0
- package/dist/resources/nodes/result.d.ts +8 -0
- package/dist/resources/nodes/result.js +11 -0
- package/dist/resources/nodes/worktree.d.ts +9 -0
- package/dist/resources/nodes/worktree.js +14 -0
- package/dist/resources/nodes.d.ts +23 -0
- package/dist/resources/nodes.js +47 -0
- package/dist/resources/providers.d.ts +51 -0
- package/dist/resources/providers.js +15 -0
- package/dist/resources/questions.d.ts +49 -0
- package/dist/resources/questions.js +21 -0
- package/dist/resources/request.d.ts +12 -0
- package/dist/resources/request.js +50 -0
- package/dist/resources/run-notice.d.ts +34 -0
- package/dist/resources/run-notice.js +67 -0
- package/dist/resources/run-reply.d.ts +79 -0
- package/dist/resources/run-reply.js +93 -0
- package/dist/resources/run-stream.d.ts +65 -0
- package/dist/resources/run-stream.js +243 -0
- package/dist/resources/run-watcher.d.ts +98 -0
- package/dist/resources/run-watcher.js +162 -0
- package/dist/resources/runs.d.ts +199 -0
- package/dist/resources/runs.js +228 -0
- package/dist/resources/shares.d.ts +40 -0
- package/dist/resources/shares.js +19 -0
- package/dist/resources/structured-requests.d.ts +14 -0
- package/dist/resources/structured-requests.js +19 -0
- package/dist/resources/uploads.d.ts +27 -0
- package/dist/resources/uploads.js +11 -0
- package/dist/schema.d.ts +7 -0
- package/dist/schema.js +10 -0
- package/dist/stores/postgres.d.ts +32 -0
- package/dist/stores/postgres.js +92 -0
- package/dist/testing/clock.d.ts +7 -0
- package/dist/testing/clock.js +12 -0
- package/dist/testing/cloud.d.ts +228 -0
- package/dist/testing/cloud.js +914 -0
- package/dist/testing/conformance/index.d.ts +2 -0
- package/dist/testing/conformance/index.js +3 -0
- package/dist/testing/conformance/keyed-write.d.ts +43 -0
- package/dist/testing/conformance/keyed-write.js +124 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.js +4 -0
- package/dist/testing/notices.d.ts +76 -0
- package/dist/testing/notices.js +209 -0
- package/dist/testing/run.d.ts +174 -0
- package/dist/testing/run.js +429 -0
- package/dist/testing/users.d.ts +127 -0
- package/dist/testing/users.js +78 -0
- package/dist/testing/wire.d.ts +62 -0
- package/dist/testing/wire.js +148 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/package.json +60 -5
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const ENV_USAGE = "Usage: crouter-sdk env --app <client id> [--out <path>]";
|
|
2
|
+
/** The variables `env` owns, in the order it writes them. `CROUTER_KEY_FILE` is never one. */
|
|
3
|
+
declare const NAMES: readonly ["CROUTER_ISSUER", "CROUTER_CLIENT_ID", "APP_ORIGIN"];
|
|
4
|
+
type Name = typeof NAMES[number];
|
|
5
|
+
export interface EnvOptions {
|
|
6
|
+
/** How long `crtr cloud apps get` may run before env gives up. */
|
|
7
|
+
crtrTimeoutMs?: number;
|
|
8
|
+
}
|
|
9
|
+
/** Rewrites each assignment to a name in `values`, appends the names it had none for, and keeps every other line. */
|
|
10
|
+
export declare function updateEnvText(text: string, values: Record<Name, string>): string;
|
|
11
|
+
/** The last `CROUTER_KEY_FILE` the file assigns, or undefined. */
|
|
12
|
+
export declare function keyFileOf(text: string): string | undefined;
|
|
13
|
+
/** The `crouter-sdk env` command. Returns the process exit code. */
|
|
14
|
+
export declare function runEnv(argv: string[], options?: EnvOptions): Promise<number>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { mkdir, open, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { CRTR_TIMEOUT_MS, notJoinedLines, parseJson, runCrtr, say } from './crtr.js';
|
|
4
|
+
export const ENV_USAGE = 'Usage: crouter-sdk env --app <client id> [--out <path>]';
|
|
5
|
+
const DEFAULT_KEY_FILE = 'local/app-key.json';
|
|
6
|
+
/** The variables `env` owns, in the order it writes them. `CROUTER_KEY_FILE` is never one. */
|
|
7
|
+
const NAMES = ['CROUTER_ISSUER', 'CROUTER_CLIENT_ID', 'APP_ORIGIN'];
|
|
8
|
+
function parseArgs(args) {
|
|
9
|
+
if (args[0] !== 'env')
|
|
10
|
+
throw new Error(ENV_USAGE);
|
|
11
|
+
let path = 'local/app.env';
|
|
12
|
+
let app;
|
|
13
|
+
const seen = new Set();
|
|
14
|
+
for (let i = 1; i < args.length; i++) {
|
|
15
|
+
const flag = args[i];
|
|
16
|
+
const value = args[++i];
|
|
17
|
+
if (seen.has(flag) || (flag !== '--out' && flag !== '--app') || !value || value.startsWith('--'))
|
|
18
|
+
throw new Error(ENV_USAGE);
|
|
19
|
+
seen.add(flag);
|
|
20
|
+
if (flag === '--out')
|
|
21
|
+
path = value;
|
|
22
|
+
else
|
|
23
|
+
app = value;
|
|
24
|
+
}
|
|
25
|
+
if (!app)
|
|
26
|
+
throw new Error(ENV_USAGE);
|
|
27
|
+
return { path, app };
|
|
28
|
+
}
|
|
29
|
+
/** One `NAME=value` assignment line, with an optional `export ` prefix. */
|
|
30
|
+
const ASSIGNMENT = /^(\s*(?:export\s+)?)([A-Za-z_][A-Za-z0-9_]*)(\s*=)(.*)$/;
|
|
31
|
+
/** A value is written bare when a dotenv reader and a shell both read it back unchanged. */
|
|
32
|
+
const formatValue = (value) => (/^[A-Za-z0-9_\-.:/@%+,~]*$/.test(value) ? value : JSON.stringify(value));
|
|
33
|
+
/** The value of a dotenv assignment's right-hand side: quotes removed, a trailing comment dropped. */
|
|
34
|
+
function readValue(raw) {
|
|
35
|
+
const text = raw.trim();
|
|
36
|
+
const quote = text[0];
|
|
37
|
+
if ((quote === '"' || quote === "'") && text.indexOf(quote, 1) > 0) {
|
|
38
|
+
const inner = text.slice(1, text.indexOf(quote, 1));
|
|
39
|
+
if (quote === "'")
|
|
40
|
+
return inner;
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(`"${inner}"`);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return inner;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return text.replace(/\s+#.*$/, '');
|
|
49
|
+
}
|
|
50
|
+
/** Rewrites each assignment to a name in `values`, appends the names it had none for, and keeps every other line. */
|
|
51
|
+
export function updateEnvText(text, values) {
|
|
52
|
+
const lines = text === '' ? [] : text.replace(/\n$/, '').split('\n');
|
|
53
|
+
const written = new Set();
|
|
54
|
+
const out = lines.map((line) => {
|
|
55
|
+
const match = ASSIGNMENT.exec(line);
|
|
56
|
+
const name = match?.[2];
|
|
57
|
+
if (!match || !name || !NAMES.includes(name))
|
|
58
|
+
return line;
|
|
59
|
+
written.add(name);
|
|
60
|
+
return `${match[1]}${name}${match[3]}${formatValue(values[name])}`;
|
|
61
|
+
});
|
|
62
|
+
for (const name of NAMES)
|
|
63
|
+
if (!written.has(name))
|
|
64
|
+
out.push(`${name}=${formatValue(values[name])}`);
|
|
65
|
+
return `${out.join('\n')}\n`;
|
|
66
|
+
}
|
|
67
|
+
/** The last `CROUTER_KEY_FILE` the file assigns, or undefined. */
|
|
68
|
+
export function keyFileOf(text) {
|
|
69
|
+
let found;
|
|
70
|
+
for (const line of text.split('\n')) {
|
|
71
|
+
const match = ASSIGNMENT.exec(line);
|
|
72
|
+
if (match?.[2] === 'CROUTER_KEY_FILE')
|
|
73
|
+
found = readValue(match[4] ?? '');
|
|
74
|
+
}
|
|
75
|
+
return found || undefined;
|
|
76
|
+
}
|
|
77
|
+
function envOf(answer) {
|
|
78
|
+
const env = answer?.env;
|
|
79
|
+
if (!env || typeof env !== 'object')
|
|
80
|
+
return undefined;
|
|
81
|
+
const values = {};
|
|
82
|
+
for (const name of NAMES) {
|
|
83
|
+
const value = env[name];
|
|
84
|
+
if (typeof value !== 'string' || !value)
|
|
85
|
+
return undefined;
|
|
86
|
+
values[name] = value;
|
|
87
|
+
}
|
|
88
|
+
return values;
|
|
89
|
+
}
|
|
90
|
+
const byHand = (path) => `Set ${NAMES.join(', ')} in ${path} by hand: \`crtr cloud apps get --app-id <client id>\` shows them under env.`;
|
|
91
|
+
/** The `crouter-sdk env` command. Returns the process exit code. */
|
|
92
|
+
export async function runEnv(argv, options = {}) {
|
|
93
|
+
const { path, app } = parseArgs(argv);
|
|
94
|
+
const outcome = await runCrtr(['cloud', 'apps', 'get', '--app-id', app, '--json'], options.crtrTimeoutMs ?? CRTR_TIMEOUT_MS);
|
|
95
|
+
if (outcome.kind === 'missing') {
|
|
96
|
+
say(["Couldn't find crtr, so nothing was written.", byHand(path)]);
|
|
97
|
+
return 2;
|
|
98
|
+
}
|
|
99
|
+
if (outcome.kind === 'not_joined') {
|
|
100
|
+
say(notJoinedLines('nothing was written', outcome.joinStep));
|
|
101
|
+
return 2;
|
|
102
|
+
}
|
|
103
|
+
if (outcome.kind === 'failed') {
|
|
104
|
+
say([`Couldn't read ${app}: ${outcome.message}`, 'Nothing was written.']);
|
|
105
|
+
return 2;
|
|
106
|
+
}
|
|
107
|
+
const values = envOf(parseJson(outcome.stdout));
|
|
108
|
+
if (!values) {
|
|
109
|
+
const last = outcome.stdout.split('\n').map((line) => line.trim()).filter(Boolean).at(-1);
|
|
110
|
+
say([`crtr answered without the app's env: ${last ?? '(no output)'}`, 'Nothing was written.']);
|
|
111
|
+
return 2;
|
|
112
|
+
}
|
|
113
|
+
const existing = await readFile(path, 'utf8').catch((error) => {
|
|
114
|
+
if (error.code === 'ENOENT')
|
|
115
|
+
return undefined;
|
|
116
|
+
throw error;
|
|
117
|
+
});
|
|
118
|
+
const next = updateEnvText(existing ?? '', values);
|
|
119
|
+
if (existing === undefined) {
|
|
120
|
+
await mkdir(dirname(path), { recursive: true });
|
|
121
|
+
const file = await open(path, 'wx', 0o600);
|
|
122
|
+
try {
|
|
123
|
+
await file.writeFile(next);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
await file.close();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else if (next !== existing) {
|
|
130
|
+
await writeFile(path, next);
|
|
131
|
+
}
|
|
132
|
+
say([
|
|
133
|
+
`Set in ${path}:`,
|
|
134
|
+
...NAMES.map((name) => ` ${name}=${values[name]}`),
|
|
135
|
+
`Key file: ${keyFileOf(next) ?? DEFAULT_KEY_FILE} (set CROUTER_KEY_FILE to change it).`,
|
|
136
|
+
]);
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { errorCodes } from '@north-light/api';
|
|
2
|
+
export type { ErrorCode, ErrorEnvelope, ErrorOrigin, ErrorType } from '@north-light/api';
|
|
3
|
+
export { mapError } from './errors.js';
|
|
4
|
+
/** Codes the SDK raises itself rather than reading from a daemon or directory response. */
|
|
5
|
+
export declare const sdkErrorCodes: readonly ["crouter_error", "id_token_invalid", "oauth_nonce_missing", "oauth_state_mismatch", "oauth_exchange_mismatch", "not_connected", "refusal_handler_reentry", "run_idle", "waiting_on_user", "request_aborted", "request_timeout", "deadline_exceeded", "connection_error", "transport_error", "daemon_unavailable", "daemon_request_interrupted", "daemon_health_unavailable", "runtime_version_unsupported", "invalid_response", "stream_error", "stream_ended"];
|
|
6
|
+
/** A code the SDK raises itself. An `APIError`'s `code` is one of these or an `ErrorCode` from the contract. */
|
|
7
|
+
export type SdkErrorCode = typeof sdkErrorCodes[number];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// The API contract owns the daemon's table; the SDK adds the codes it raises itself.
|
|
2
|
+
export { errorCodes } from '@north-light/api';
|
|
3
|
+
export { mapError } from './errors.js';
|
|
4
|
+
/** Codes the SDK raises itself rather than reading from a daemon or directory response. */
|
|
5
|
+
export const sdkErrorCodes = [
|
|
6
|
+
/** A `CrouterError` configuration or protocol failure with no more specific code. */
|
|
7
|
+
'crouter_error',
|
|
8
|
+
/** An ID token was missing, failed verification, or its nonce did not match. */
|
|
9
|
+
'id_token_invalid',
|
|
10
|
+
/** `exchangeIdentity` was called without the nonce `authorizeUrl` returned. */
|
|
11
|
+
'oauth_nonce_missing',
|
|
12
|
+
/** The callback's `state` was missing or differed from the saved one. */
|
|
13
|
+
'oauth_state_mismatch',
|
|
14
|
+
/** `exchangeCode` got a sign-in-only answer, or `exchangeIdentity` got one carrying a connection. */
|
|
15
|
+
'oauth_exchange_mismatch',
|
|
16
|
+
/** `oauth.client(userId)` or `refresh` found no stored connection for the person: they must sign in again. */
|
|
17
|
+
'not_connected',
|
|
18
|
+
/** `onRefusal` called `oauth.client` (or `refresh`) for the user whose dead connection it is handling. */
|
|
19
|
+
'refusal_handler_reentry',
|
|
20
|
+
/** `runs.parse`: the run stopped without settling. */
|
|
21
|
+
'run_idle',
|
|
22
|
+
/** `runs.parse`: the run waits on a question and no `onQuestion` was given. */
|
|
23
|
+
'waiting_on_user',
|
|
24
|
+
/** The caller's `AbortSignal` fired (`APIUserAbortError`). */
|
|
25
|
+
'request_aborted',
|
|
26
|
+
/** The request exceeded its timeout (`APIConnectionTimeoutError`). */
|
|
27
|
+
'request_timeout',
|
|
28
|
+
/** The call's overall deadline passed, or a retry wait would have passed it (`DeadlineExceededError`). */
|
|
29
|
+
'deadline_exceeded',
|
|
30
|
+
/** The daemon could not be reached, or the connection failed (`APIConnectionError`). */
|
|
31
|
+
'connection_error',
|
|
32
|
+
'transport_error',
|
|
33
|
+
'daemon_unavailable',
|
|
34
|
+
'daemon_request_interrupted',
|
|
35
|
+
'daemon_health_unavailable',
|
|
36
|
+
/** The runtime is older than this SDK, or sent no `Crouter-Runtime-Version` (`RuntimeVersionError`). */
|
|
37
|
+
'runtime_version_unsupported',
|
|
38
|
+
/** A response the SDK could not read as the API's JSON or error envelope. */
|
|
39
|
+
'invalid_response',
|
|
40
|
+
/** An event stream failed, or ended before the reply or node it carried did. */
|
|
41
|
+
'stream_error',
|
|
42
|
+
'stream_ended',
|
|
43
|
+
];
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { APIError } from '@north-light/api';
|
|
2
|
+
import type { SdkErrorCode } from './error-codes.js';
|
|
3
|
+
export { APIError };
|
|
4
|
+
/** An SDK configuration or protocol error, raised by the SDK itself. Its `code` is an `SdkErrorCode`. */
|
|
5
|
+
export declare class CrouterError extends APIError {
|
|
6
|
+
readonly code: SdkErrorCode;
|
|
7
|
+
constructor(message: string, code?: SdkErrorCode);
|
|
8
|
+
}
|
|
9
|
+
/** The runtime is older than this SDK (or names no version at all), so the SDK's types do not describe what it
|
|
10
|
+
* sends. Raised before the SDK reads the response; never retryable. Upgrade the runtime (or pin the SDK to the
|
|
11
|
+
* runtime's release). `code` is `runtime_version_unsupported`, `status` 426. */
|
|
12
|
+
export declare class RuntimeVersionError extends APIError {
|
|
13
|
+
/** The oldest runtime this SDK accepts: its own release version. */
|
|
14
|
+
get sdkVersion(): string;
|
|
15
|
+
/** The version the runtime named in `Crouter-Runtime-Version`, or `null` when it sent none (a runtime from before the header existed). */
|
|
16
|
+
get runtimeVersion(): string | null;
|
|
17
|
+
}
|
|
18
|
+
/** `RuntimeVersionError.details`. */
|
|
19
|
+
export interface RuntimeVersionDetails {
|
|
20
|
+
sdk_version: string;
|
|
21
|
+
runtime_version: string | null;
|
|
22
|
+
}
|
|
23
|
+
/** The daemon rejected the request: HTTP 400. */
|
|
24
|
+
export declare class BadRequestError extends APIError {
|
|
25
|
+
}
|
|
26
|
+
/** The token was missing or wrong: HTTP 401. */
|
|
27
|
+
export declare class AuthenticationError extends APIError {
|
|
28
|
+
}
|
|
29
|
+
/** The caller is not allowed to do this: HTTP 403. */
|
|
30
|
+
export declare class PermissionDeniedError extends APIError {
|
|
31
|
+
}
|
|
32
|
+
/** No such node, profile, cron, or other resource: HTTP 404. */
|
|
33
|
+
export declare class NotFoundError extends APIError {
|
|
34
|
+
}
|
|
35
|
+
/** The resource is in a state that forbids the request: HTTP 409. */
|
|
36
|
+
export declare class ConflictError extends APIError {
|
|
37
|
+
}
|
|
38
|
+
/** The request was well-formed but unusable, or too large: HTTP 413 and 422. */
|
|
39
|
+
export declare class UnprocessableEntityError extends APIError {
|
|
40
|
+
}
|
|
41
|
+
/** Too many requests: HTTP 429. */
|
|
42
|
+
export declare class RateLimitError extends APIError {
|
|
43
|
+
}
|
|
44
|
+
/** The daemon failed while handling the request: HTTP 500 and above. */
|
|
45
|
+
export declare class InternalServerError extends APIError {
|
|
46
|
+
}
|
|
47
|
+
/** The daemon could not be reached, or the connection failed mid-request. */
|
|
48
|
+
export declare class APIConnectionError extends APIError {
|
|
49
|
+
}
|
|
50
|
+
/** The request exceeded its timeout before the daemon answered. */
|
|
51
|
+
export declare class APIConnectionTimeoutError extends APIConnectionError {
|
|
52
|
+
}
|
|
53
|
+
/** The call's overall deadline (`budgetMs` / `deadlineAt`) passed, or the next retry wait would have passed it.
|
|
54
|
+
* `status` 504, `code` `deadline_exceeded`. `deadlineAt` is the instant (epoch ms); `reason` is `elapsed` (time ran out)
|
|
55
|
+
* or `retry_after` (the server asked for a wait longer than the time left, so the call stopped without sleeping;
|
|
56
|
+
* `retryAfterS` is that request). `cause` is the last failure, when there was one. */
|
|
57
|
+
export declare class DeadlineExceededError extends APIConnectionTimeoutError {
|
|
58
|
+
get deadlineAt(): number;
|
|
59
|
+
get reason(): 'elapsed' | 'retry_after';
|
|
60
|
+
get retryAfterRequestS(): number | undefined;
|
|
61
|
+
}
|
|
62
|
+
/** `DeadlineExceededError.details`. */
|
|
63
|
+
export interface DeadlineDetails {
|
|
64
|
+
deadline_at: number;
|
|
65
|
+
reason: 'elapsed' | 'retry_after';
|
|
66
|
+
retry_after_s?: number;
|
|
67
|
+
}
|
|
68
|
+
/** The caller's `AbortSignal` fired. */
|
|
69
|
+
export declare class APIUserAbortError extends APIError {
|
|
70
|
+
}
|
|
71
|
+
/** Maps a transport failure or a plain `APIError` onto the specific class for its status and code. */
|
|
72
|
+
export declare function mapError(error: unknown): APIError;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { APIError } from '@north-light/api';
|
|
2
|
+
export { APIError };
|
|
3
|
+
/** An SDK configuration or protocol error, raised by the SDK itself. Its `code` is an `SdkErrorCode`. */
|
|
4
|
+
export class CrouterError extends APIError {
|
|
5
|
+
constructor(message, code = 'crouter_error') {
|
|
6
|
+
super(0, code, message);
|
|
7
|
+
this.name = 'CrouterError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/** The runtime is older than this SDK (or names no version at all), so the SDK's types do not describe what it
|
|
11
|
+
* sends. Raised before the SDK reads the response; never retryable. Upgrade the runtime (or pin the SDK to the
|
|
12
|
+
* runtime's release). `code` is `runtime_version_unsupported`, `status` 426. */
|
|
13
|
+
export class RuntimeVersionError extends APIError {
|
|
14
|
+
/** The oldest runtime this SDK accepts: its own release version. */
|
|
15
|
+
get sdkVersion() { return this.details.sdk_version; }
|
|
16
|
+
/** The version the runtime named in `Crouter-Runtime-Version`, or `null` when it sent none (a runtime from before the header existed). */
|
|
17
|
+
get runtimeVersion() { return this.details.runtime_version; }
|
|
18
|
+
}
|
|
19
|
+
/** The daemon rejected the request: HTTP 400. */
|
|
20
|
+
export class BadRequestError extends APIError {
|
|
21
|
+
}
|
|
22
|
+
/** The token was missing or wrong: HTTP 401. */
|
|
23
|
+
export class AuthenticationError extends APIError {
|
|
24
|
+
}
|
|
25
|
+
/** The caller is not allowed to do this: HTTP 403. */
|
|
26
|
+
export class PermissionDeniedError extends APIError {
|
|
27
|
+
}
|
|
28
|
+
/** No such node, profile, cron, or other resource: HTTP 404. */
|
|
29
|
+
export class NotFoundError extends APIError {
|
|
30
|
+
}
|
|
31
|
+
/** The resource is in a state that forbids the request: HTTP 409. */
|
|
32
|
+
export class ConflictError extends APIError {
|
|
33
|
+
}
|
|
34
|
+
/** The request was well-formed but unusable, or too large: HTTP 413 and 422. */
|
|
35
|
+
export class UnprocessableEntityError extends APIError {
|
|
36
|
+
}
|
|
37
|
+
/** Too many requests: HTTP 429. */
|
|
38
|
+
export class RateLimitError extends APIError {
|
|
39
|
+
}
|
|
40
|
+
/** The daemon failed while handling the request: HTTP 500 and above. */
|
|
41
|
+
export class InternalServerError extends APIError {
|
|
42
|
+
}
|
|
43
|
+
/** The daemon could not be reached, or the connection failed mid-request. */
|
|
44
|
+
export class APIConnectionError extends APIError {
|
|
45
|
+
}
|
|
46
|
+
/** The request exceeded its timeout before the daemon answered. */
|
|
47
|
+
export class APIConnectionTimeoutError extends APIConnectionError {
|
|
48
|
+
}
|
|
49
|
+
/** The call's overall deadline (`budgetMs` / `deadlineAt`) passed, or the next retry wait would have passed it.
|
|
50
|
+
* `status` 504, `code` `deadline_exceeded`. `deadlineAt` is the instant (epoch ms); `reason` is `elapsed` (time ran out)
|
|
51
|
+
* or `retry_after` (the server asked for a wait longer than the time left, so the call stopped without sleeping;
|
|
52
|
+
* `retryAfterS` is that request). `cause` is the last failure, when there was one. */
|
|
53
|
+
export class DeadlineExceededError extends APIConnectionTimeoutError {
|
|
54
|
+
get deadlineAt() { return this.details.deadline_at; }
|
|
55
|
+
get reason() { return this.details.reason; }
|
|
56
|
+
get retryAfterRequestS() { return this.details.retry_after_s; }
|
|
57
|
+
}
|
|
58
|
+
/** The caller's `AbortSignal` fired. */
|
|
59
|
+
export class APIUserAbortError extends APIError {
|
|
60
|
+
}
|
|
61
|
+
/** Maps a transport failure or a plain `APIError` onto the specific class for its status and code. */
|
|
62
|
+
export function mapError(error) {
|
|
63
|
+
if (!(error instanceof APIError) || error.constructor !== APIError) {
|
|
64
|
+
return error instanceof APIError ? error : new APIConnectionError(0, 'connection_error', error instanceof Error ? error.message : String(error));
|
|
65
|
+
}
|
|
66
|
+
const Constructor = error.code === 'runtime_version_unsupported'
|
|
67
|
+
? RuntimeVersionError
|
|
68
|
+
: error.code === 'deadline_exceeded'
|
|
69
|
+
? DeadlineExceededError
|
|
70
|
+
: error.code === 'request_aborted'
|
|
71
|
+
? APIUserAbortError
|
|
72
|
+
: error.status === 504 || error.code === 'request_timeout'
|
|
73
|
+
? APIConnectionTimeoutError
|
|
74
|
+
: error.code === 'daemon_unavailable' || error.code === 'transport_error' || error.code === 'daemon_request_interrupted' || error.code === 'daemon_health_unavailable'
|
|
75
|
+
? APIConnectionError
|
|
76
|
+
: error.status === 400
|
|
77
|
+
? BadRequestError
|
|
78
|
+
: error.status === 401
|
|
79
|
+
? AuthenticationError
|
|
80
|
+
: error.status === 403
|
|
81
|
+
? PermissionDeniedError
|
|
82
|
+
: error.status === 404
|
|
83
|
+
? NotFoundError
|
|
84
|
+
: error.status === 409
|
|
85
|
+
? ConflictError
|
|
86
|
+
: error.status === 413 || error.status === 422
|
|
87
|
+
? UnprocessableEntityError
|
|
88
|
+
: error.status === 402 || error.status === 429
|
|
89
|
+
? RateLimitError
|
|
90
|
+
: error.status >= 500
|
|
91
|
+
? InternalServerError
|
|
92
|
+
: APIError;
|
|
93
|
+
const mapped = new Constructor(error.status, error.code, error.message, error.details, error.headers, {
|
|
94
|
+
type: error.type, param: error.param, origin: error.origin, retryable: error.retryable,
|
|
95
|
+
retry_after_s: error.retryAfterS, reset_at: error.resetAt, request_id: error.requestId, run_id: error.runId,
|
|
96
|
+
question_id: error.questionId, scope: error.scope, user_action: error.userAction,
|
|
97
|
+
});
|
|
98
|
+
if (error.cause !== undefined)
|
|
99
|
+
Object.defineProperty(mapped, 'cause', { value: error.cause, configurable: true, writable: true });
|
|
100
|
+
return mapped;
|
|
101
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export { Crouter as default, Crouter } from './client.js';
|
|
2
|
+
export type { CrouterOptions } from './client.js';
|
|
3
|
+
export { NodeStream } from './resources/node-stream.js';
|
|
4
|
+
export { RunStream } from './resources/run-stream.js';
|
|
5
|
+
export { CustomObjects } from './resources/custom-objects.js';
|
|
6
|
+
export type { CustomDeliveryStream, ReceiveOptions } from './resources/custom-objects.js';
|
|
7
|
+
export type { RunEvent, RunEventListener, RunEventOf, RunEventType, RunStreamEnd, UnknownRunEvent } from './resources/run-stream.js';
|
|
8
|
+
export { RunReply } from './resources/run-reply.js';
|
|
9
|
+
export type { RunReplyCollectOptions, RunReplyEnding, RunReplyEvent, RunReplyResult, SentRunMessage } from './resources/run-reply.js';
|
|
10
|
+
export type { AppProfile, RunActivity, RunDeleteResult, RunObject, RunParsedOutcome, RunParseOptions, RunParseParams, RunStartParams, RunStartResult, RunStatus, RunTrace } from './resources/runs.js';
|
|
11
|
+
export type { FileDownloadOptions } from './resources/files.js';
|
|
12
|
+
export type { Upload, UploadCreateParams } from './resources/uploads.js';
|
|
13
|
+
export type { AttachmentLink } from './resources/attachments.js';
|
|
14
|
+
export type { Share, ShareCreated, ShareCreateParams } from './resources/shares.js';
|
|
15
|
+
export type { MemoryDocument, MemoryGetParams, MemoryListItem, MemoryListPage, MemoryListParams } from './resources/memory.js';
|
|
16
|
+
export type { Question, QuestionAnswered, QuestionFields, QuestionResponses, QuestionSlot, QuestionStatus } from './resources/questions.js';
|
|
17
|
+
export type { ProviderToolEntry } from './resources/providers.js';
|
|
18
|
+
export { OAuth2Client, ConnectedCrouter, DEFAULT_DIRECTORY_TIMEOUT_MS, MemoryConnectionStore, profileFromClaims } from './oauth/index.js';
|
|
19
|
+
export { refusalOf } from './oauth/index.js';
|
|
20
|
+
export type { RefreshFailure, Refusal, RefusalKind } from './oauth/index.js';
|
|
21
|
+
export { RunWatcher } from './resources/run-watcher.js';
|
|
22
|
+
export type { PendingRun, RunActivityEvent, RunEnded, RunEnding, RunWatchOptions } from './resources/run-watcher.js';
|
|
23
|
+
export { GrantWatcher } from './resources/grant-watcher.js';
|
|
24
|
+
export type { GrantChanged, GrantWatchOptions } from './resources/grant-watcher.js';
|
|
25
|
+
export { generateKeyPair } from './oauth/keygen.js';
|
|
26
|
+
export { parsePrivateJwk, privateKeyFromFile } from './oauth/keygen.js';
|
|
27
|
+
export type { AuthorizeNonce, Connection, ConnectionStore, Identity, OAuth2Options } from './oauth/index.js';
|
|
28
|
+
export type { IdTokenClaims } from '@north-light/identity';
|
|
29
|
+
export { cursorFromRequest, forwardRunEvents } from './resources/forward.js';
|
|
30
|
+
export { RUN_NOTICE_SCOPE, RunNoticeError, verifyRunNotice } from './resources/run-notice.js';
|
|
31
|
+
export type { RunNotice, VerifyRunNoticeOptions } from './resources/run-notice.js';
|
|
32
|
+
export type { NodeStreamEvent, NodeStreamEventListener, NodeStreamEventType } from './resources/node-stream.js';
|
|
33
|
+
export { describeToolDefault, followActivity } from './resources/activity.js';
|
|
34
|
+
export type { ActivityStep, DescribeTool } from './resources/activity.js';
|
|
35
|
+
export { APIError, APIConnectionError, APIConnectionTimeoutError, APIUserAbortError, AuthenticationError, BadRequestError, ConflictError, CrouterError, DeadlineExceededError, InternalServerError, NotFoundError, PermissionDeniedError, RateLimitError, RuntimeVersionError, UnprocessableEntityError, mapError, } from './errors.js';
|
|
36
|
+
export type { DeadlineDetails, RuntimeVersionDetails } from './errors.js';
|
|
37
|
+
export { SDK_VERSION } from './version.js';
|
|
38
|
+
export { sdkErrorCodes } from './error-codes.js';
|
|
39
|
+
export type { SdkErrorCode } from './error-codes.js';
|
|
40
|
+
export type { AuthStatus, AuthStatusParams, CancelParams, EnsureProfileParams, JsonSchema, MessageParams, NodeCreateParams, NodeEventsOptions, NodeOutcomeOptions, OutputSchema, ParsedOutcome, RequestOptions, SchemaOutput, } from './types.js';
|
|
41
|
+
export type * from '@north-light/api';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { Crouter as default, Crouter } from './client.js';
|
|
2
|
+
export { NodeStream } from './resources/node-stream.js';
|
|
3
|
+
export { RunStream } from './resources/run-stream.js';
|
|
4
|
+
export { CustomObjects } from './resources/custom-objects.js';
|
|
5
|
+
export { RunReply } from './resources/run-reply.js';
|
|
6
|
+
export { OAuth2Client, ConnectedCrouter, DEFAULT_DIRECTORY_TIMEOUT_MS, MemoryConnectionStore, profileFromClaims } from './oauth/index.js';
|
|
7
|
+
export { refusalOf } from './oauth/index.js';
|
|
8
|
+
export { RunWatcher } from './resources/run-watcher.js';
|
|
9
|
+
export { GrantWatcher } from './resources/grant-watcher.js';
|
|
10
|
+
export { generateKeyPair } from './oauth/keygen.js';
|
|
11
|
+
export { parsePrivateJwk, privateKeyFromFile } from './oauth/keygen.js';
|
|
12
|
+
export { cursorFromRequest, forwardRunEvents } from './resources/forward.js';
|
|
13
|
+
export { RUN_NOTICE_SCOPE, RunNoticeError, verifyRunNotice } from './resources/run-notice.js';
|
|
14
|
+
export { describeToolDefault, followActivity } from './resources/activity.js';
|
|
15
|
+
export { APIError, APIConnectionError, APIConnectionTimeoutError, APIUserAbortError, AuthenticationError, BadRequestError, ConflictError, CrouterError, DeadlineExceededError, InternalServerError, NotFoundError, PermissionDeniedError, RateLimitError, RuntimeVersionError, UnprocessableEntityError, mapError, } from './errors.js';
|
|
16
|
+
export { SDK_VERSION } from './version.js';
|
|
17
|
+
export { sdkErrorCodes } from './error-codes.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The `crouter-sdk` bin: `keygen` and `env`. The file keeps its first command's name because the
|
|
3
|
+
// published bin path (`dist/keygen-cli.js`) is part of the package.
|
|
4
|
+
import { runEnv } from './env-command.js';
|
|
5
|
+
import { runKeygen } from './keygen-command.js';
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const run = args[0] === 'env' ? runEnv(args) : runKeygen(args);
|
|
8
|
+
run.then((code) => { process.exitCode = code; }, (error) => {
|
|
9
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
10
|
+
process.exitCode = 1;
|
|
11
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface KeygenOptions {
|
|
2
|
+
/** How long `crtr cloud apps keys add` may run before keygen gives up. */
|
|
3
|
+
crtrTimeoutMs?: number;
|
|
4
|
+
}
|
|
5
|
+
/** The `crouter-sdk keygen` command. Returns the process exit code. */
|
|
6
|
+
export declare function runKeygen(argv: string[], options?: KeygenOptions): Promise<number>;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { mkdir, open, unlink } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
import { CRTR_TIMEOUT_MS, notJoinedLines, parseJson, runCrtr, say } from './crtr.js';
|
|
5
|
+
import { generateKeyPair } from './oauth/keygen.js';
|
|
6
|
+
const USAGE = 'Usage: crouter-sdk keygen [--out <path>] [--app <client id>] [--yes]';
|
|
7
|
+
function parseArgs(args) {
|
|
8
|
+
if (args[0] !== 'keygen')
|
|
9
|
+
throw new Error(USAGE);
|
|
10
|
+
const parsed = { path: 'local/app-key.json', yes: false };
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
for (let i = 1; i < args.length; i++) {
|
|
13
|
+
const flag = args[i];
|
|
14
|
+
if (seen.has(flag))
|
|
15
|
+
throw new Error(USAGE);
|
|
16
|
+
seen.add(flag);
|
|
17
|
+
if (flag === '--yes') {
|
|
18
|
+
parsed.yes = true;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const value = args[++i];
|
|
22
|
+
if ((flag !== '--out' && flag !== '--app') || !value || value.startsWith('--'))
|
|
23
|
+
throw new Error(USAGE);
|
|
24
|
+
if (flag === '--out')
|
|
25
|
+
parsed.path = value;
|
|
26
|
+
else
|
|
27
|
+
parsed.app = value;
|
|
28
|
+
}
|
|
29
|
+
return parsed;
|
|
30
|
+
}
|
|
31
|
+
const manualSteps = (path) => [
|
|
32
|
+
'Next, in this order:',
|
|
33
|
+
" 1. Add the public JWK printed on stdout as a key: click Add key on your app's Sign-in tab in the console, or paste it under Register an app for a new app. Adding a key never replaces your current one.",
|
|
34
|
+
` 2. Then point your app's privateKey at ${path} and deploy.`,
|
|
35
|
+
' 3. Once a real sign-in works, retire the old key on the Sign-in tab.',
|
|
36
|
+
];
|
|
37
|
+
const addedSteps = (path, app) => [
|
|
38
|
+
'Next, in this order:',
|
|
39
|
+
` 1. Point your app's privateKey at ${path} and deploy.`,
|
|
40
|
+
' 2. Check that a real sign-in works.',
|
|
41
|
+
` 3. Retire the old key: on the app's Sign-in tab, or \`crtr cloud apps keys retire --app-id ${app} --kid <old kid>\` (\`crtr cloud apps keys list --app-id ${app}\` shows the kids).`,
|
|
42
|
+
];
|
|
43
|
+
/** Asks on stderr; resolves true only for Enter or y/yes. End of input declines. */
|
|
44
|
+
function confirm(question) {
|
|
45
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
let answered = false;
|
|
48
|
+
rl.on('close', () => { if (!answered) {
|
|
49
|
+
process.stderr.write('\n');
|
|
50
|
+
resolve(false);
|
|
51
|
+
} });
|
|
52
|
+
rl.question(`${question} `, (answer) => {
|
|
53
|
+
answered = true;
|
|
54
|
+
rl.close();
|
|
55
|
+
resolve(/^(y|yes)?$/i.test(answer.trim()));
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** Runs `crtr cloud apps keys add` once. */
|
|
60
|
+
async function addKey(app, publicJwk, timeoutMs) {
|
|
61
|
+
const outcome = await runCrtr(['cloud', 'apps', 'keys', 'add', '--app-id', app, '--jwk', publicJwk, '--json'], timeoutMs);
|
|
62
|
+
if (outcome.kind !== 'ok')
|
|
63
|
+
return outcome;
|
|
64
|
+
const answer = parseJson(outcome.stdout);
|
|
65
|
+
if (typeof answer?.kid !== 'string') {
|
|
66
|
+
const last = outcome.stdout.split('\n').map((line) => line.trim()).filter(Boolean).at(-1);
|
|
67
|
+
return { kind: 'failed', message: `crtr answered without the added key: ${last ?? '(no output)'}` };
|
|
68
|
+
}
|
|
69
|
+
return { kind: 'added', kid: answer.kid, created: answer.created !== false };
|
|
70
|
+
}
|
|
71
|
+
/** The `crouter-sdk keygen` command. Returns the process exit code. */
|
|
72
|
+
export async function runKeygen(argv, options = {}) {
|
|
73
|
+
const { path, app, yes } = parseArgs(argv);
|
|
74
|
+
await mkdir(dirname(path), { recursive: true });
|
|
75
|
+
// Reserve the destination before generating or printing the public key; never truncate an existing key.
|
|
76
|
+
const file = await open(path, 'wx', 0o600).catch((error) => {
|
|
77
|
+
if (error.code === 'EEXIST')
|
|
78
|
+
throw new Error(`${path} already exists. Delete it or pass --out to write somewhere else.`);
|
|
79
|
+
throw error;
|
|
80
|
+
});
|
|
81
|
+
let publicJwk;
|
|
82
|
+
let kid;
|
|
83
|
+
try {
|
|
84
|
+
const pair = await generateKeyPair();
|
|
85
|
+
await file.writeFile(`${JSON.stringify(pair.privateJwk)}\n`);
|
|
86
|
+
({ publicJwk, kid } = pair);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
await file.close();
|
|
90
|
+
await unlink(path);
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
await file.close();
|
|
94
|
+
const publicLine = JSON.stringify(publicJwk);
|
|
95
|
+
console.error(`Saved the private key to ${path} (key id ${kid}). Keep it out of git.`);
|
|
96
|
+
console.log(publicLine);
|
|
97
|
+
if (!app) {
|
|
98
|
+
say([...manualSteps(path), 'Tip: pass --app <client id> to add the key with crtr.']);
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
if (!yes) {
|
|
102
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
103
|
+
say(['Not adding the key: no terminal to confirm. Pass --yes to add it with crtr.', ...manualSteps(path)]);
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
if (!(await confirm(`Add this key to ${app} as an additional key? Your current key keeps working. [Y/n]`))) {
|
|
107
|
+
say(manualSteps(path));
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const outcome = await addKey(app, publicLine, options.crtrTimeoutMs ?? CRTR_TIMEOUT_MS);
|
|
112
|
+
if (outcome.kind === 'missing') {
|
|
113
|
+
say(["Couldn't find crtr, so the key was not added.", ...manualSteps(path)]);
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
if (outcome.kind === 'not_joined') {
|
|
117
|
+
say([...notJoinedLines('the key was not added', outcome.joinStep), ...manualSteps(path)]);
|
|
118
|
+
return 2;
|
|
119
|
+
}
|
|
120
|
+
if (outcome.kind === 'failed') {
|
|
121
|
+
say([`Couldn't add the key: ${outcome.message}`, ...manualSteps(path)]);
|
|
122
|
+
return 2;
|
|
123
|
+
}
|
|
124
|
+
say([
|
|
125
|
+
outcome.created
|
|
126
|
+
? `Added key ${outcome.kid} to ${app} as an additional key. Your current key still works.`
|
|
127
|
+
: `Key ${outcome.kid} is already on ${app}.`,
|
|
128
|
+
...addedSteps(path, app),
|
|
129
|
+
]);
|
|
130
|
+
return 0;
|
|
131
|
+
}
|