@north-light/sdk 0.0.0 → 0.3.414
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 +162 -0
- package/dist/client.js +543 -0
- package/dist/error-codes.d.ts +7 -0
- package/dist/error-codes.js +41 -0
- package/dist/errors.d.ts +57 -0
- package/dist/errors.js +87 -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 +6 -0
- package/dist/keygen-command.d.ts +6 -0
- package/dist/keygen-command.js +157 -0
- package/dist/oauth/index.d.ts +217 -0
- package/dist/oauth/index.js +535 -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 +7 -0
- package/dist/resources/request.js +19 -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 +192 -0
- package/dist/resources/runs.js +208 -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/cloud.d.ts +209 -0
- package/dist/testing/cloud.js +880 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.js +3 -0
- package/dist/testing/notices.d.ts +70 -0
- package/dist/testing/notices.js +205 -0
- package/dist/testing/run.d.ts +172 -0
- package/dist/testing/run.js +429 -0
- package/dist/testing/users.d.ts +121 -0
- package/dist/testing/users.js +70 -0
- package/dist/testing/wire.d.ts +62 -0
- package/dist/testing/wire.js +148 -0
- package/dist/types.d.ts +80 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/package.json +56 -5
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { RequestOptions } from '../types.js';
|
|
2
|
+
import type { Request } from './request.js';
|
|
3
|
+
/** `shares.create` input. */
|
|
4
|
+
export interface ShareCreateParams {
|
|
5
|
+
/** Absolute path of a file the caller may read. Without `crtr:files:share`, only files in the caller's own app space. */
|
|
6
|
+
path: string;
|
|
7
|
+
/** ISO 8601 time in the future; absent or null keeps the share until it is deleted. */
|
|
8
|
+
expires_at?: string | null;
|
|
9
|
+
}
|
|
10
|
+
/** A newly created share: a frozen copy of the file at a public URL. */
|
|
11
|
+
export interface ShareCreated {
|
|
12
|
+
share_id: string;
|
|
13
|
+
url: string;
|
|
14
|
+
expires_at: string | null;
|
|
15
|
+
}
|
|
16
|
+
/** A share the caller created. */
|
|
17
|
+
export interface Share {
|
|
18
|
+
share_id: string;
|
|
19
|
+
url: string;
|
|
20
|
+
source_path: string;
|
|
21
|
+
created_by: string;
|
|
22
|
+
created_at: string;
|
|
23
|
+
expires_at: string | null;
|
|
24
|
+
status: 'on' | 'off';
|
|
25
|
+
}
|
|
26
|
+
/** Public share links (app listener only). List and delete act on the caller's own shares. */
|
|
27
|
+
export declare class Shares {
|
|
28
|
+
private readonly request;
|
|
29
|
+
constructor(request: Request);
|
|
30
|
+
/** Never retried: a retry could create a second share. */
|
|
31
|
+
create(params: ShareCreateParams, options?: RequestOptions): Promise<ShareCreated>;
|
|
32
|
+
list(options?: RequestOptions): Promise<{
|
|
33
|
+
shares: Share[];
|
|
34
|
+
}>;
|
|
35
|
+
/** Turn a share off and delete its stored copy. */
|
|
36
|
+
delete(shareId: string, options?: RequestOptions): Promise<{
|
|
37
|
+
share_id: string;
|
|
38
|
+
status: 'off';
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { pathSegment } from './identifiers.js';
|
|
2
|
+
/** Public share links (app listener only). List and delete act on the caller's own shares. */
|
|
3
|
+
export class Shares {
|
|
4
|
+
request;
|
|
5
|
+
constructor(request) {
|
|
6
|
+
this.request = request;
|
|
7
|
+
}
|
|
8
|
+
/** Never retried: a retry could create a second share. */
|
|
9
|
+
create(params, options = {}) {
|
|
10
|
+
return this.request('POST', '/v1/shares', params, { ...options, maxRetries: 0 });
|
|
11
|
+
}
|
|
12
|
+
list(options) {
|
|
13
|
+
return this.request('GET', '/v1/shares', undefined, options);
|
|
14
|
+
}
|
|
15
|
+
/** Turn a share off and delete its stored copy. */
|
|
16
|
+
delete(shareId, options) {
|
|
17
|
+
return this.request('DELETE', `/v1/shares/${encodeURIComponent(pathSegment(shareId, 'share id'))}`, undefined, options);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CreateStructuredRequest, CreateStructuredRequestResponse, StructuredRequestDTO } from '@north-light/api';
|
|
2
|
+
import type { RequestOptions } from '../types.js';
|
|
3
|
+
import type { Request } from './request.js';
|
|
4
|
+
/** Durable structured requests: a schema-bound question to a new or existing node, settled once. */
|
|
5
|
+
export declare class StructuredRequests {
|
|
6
|
+
private readonly request;
|
|
7
|
+
constructor(request: Request);
|
|
8
|
+
create(body: CreateStructuredRequest, options?: RequestOptions): Promise<CreateStructuredRequestResponse>;
|
|
9
|
+
/** `wait` (0–25 seconds) long-polls until the request settles. */
|
|
10
|
+
retrieve(requestId: string, query?: {
|
|
11
|
+
wait?: number;
|
|
12
|
+
}, options?: RequestOptions): Promise<StructuredRequestDTO>;
|
|
13
|
+
cancel(requestId: string, options?: RequestOptions): Promise<StructuredRequestDTO>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { pathSegment } from './identifiers.js';
|
|
2
|
+
import { withQuery } from './request.js';
|
|
3
|
+
/** Durable structured requests: a schema-bound question to a new or existing node, settled once. */
|
|
4
|
+
export class StructuredRequests {
|
|
5
|
+
request;
|
|
6
|
+
constructor(request) {
|
|
7
|
+
this.request = request;
|
|
8
|
+
}
|
|
9
|
+
create(body, options) {
|
|
10
|
+
return this.request('POST', '/v1/structured-requests', body, options);
|
|
11
|
+
}
|
|
12
|
+
/** `wait` (0–25 seconds) long-polls until the request settles. */
|
|
13
|
+
retrieve(requestId, query, options) {
|
|
14
|
+
return this.request('GET', withQuery(`/v1/structured-requests/${pathSegment(requestId, 'request id')}`, query), undefined, options);
|
|
15
|
+
}
|
|
16
|
+
cancel(requestId, options) {
|
|
17
|
+
return this.request('POST', `/v1/structured-requests/${pathSegment(requestId, 'request id')}/cancel`, {}, options);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RequestOptions } from '../types.js';
|
|
2
|
+
import type { Request } from './request.js';
|
|
3
|
+
/** `uploads.create` input: the file the caller will PUT to `upload_link`. At most 100 MiB. */
|
|
4
|
+
export interface UploadCreateParams {
|
|
5
|
+
/** File name, stored as the last segment of `url`. */
|
|
6
|
+
name: string;
|
|
7
|
+
/** The media type the bytes are PUT with. */
|
|
8
|
+
media_type: string;
|
|
9
|
+
/** Exact byte length, a non-negative integer. */
|
|
10
|
+
size: number;
|
|
11
|
+
}
|
|
12
|
+
/** A reserved upload. PUT the bytes to `upload_link` before `expires_at`, then put `url` in a run prompt or message. */
|
|
13
|
+
export interface Upload {
|
|
14
|
+
upload_id: string;
|
|
15
|
+
/** The storage URL a run's prompt or message names; the runtime fetches it into the run's `attachments/`. */
|
|
16
|
+
url: string;
|
|
17
|
+
/** Signed PUT link for the bytes. */
|
|
18
|
+
upload_link: string;
|
|
19
|
+
expires_at: string;
|
|
20
|
+
}
|
|
21
|
+
/** Uploads to the runtime's storage (app listener only). */
|
|
22
|
+
export declare class Uploads {
|
|
23
|
+
private readonly request;
|
|
24
|
+
constructor(request: Request);
|
|
25
|
+
/** Reserve an upload. Never retried: a retry would reserve a second upload. */
|
|
26
|
+
create(params: UploadCreateParams, options?: RequestOptions): Promise<Upload>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Uploads to the runtime's storage (app listener only). */
|
|
2
|
+
export class Uploads {
|
|
3
|
+
request;
|
|
4
|
+
constructor(request) {
|
|
5
|
+
this.request = request;
|
|
6
|
+
}
|
|
7
|
+
/** Reserve an upload. Never retried: a retry would reserve a second upload. */
|
|
8
|
+
create(params, options = {}) {
|
|
9
|
+
return this.request('POST', '/v1/uploads', params, { ...options, maxRetries: 0 });
|
|
10
|
+
}
|
|
11
|
+
}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { JsonSchema } from './types.js';
|
|
2
|
+
/** Accept a plain JSON Schema object or anything with `toJSONSchema()` (a zod
|
|
3
|
+
* v4 object satisfies this directly) and resolve it to the plain object the
|
|
4
|
+
* wire format (`output_schema`, a JSON string) needs. */
|
|
5
|
+
export declare function resolveJsonSchema(schema: JsonSchema | {
|
|
6
|
+
toJSONSchema(): JsonSchema;
|
|
7
|
+
}): JsonSchema;
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Accept a plain JSON Schema object or anything with `toJSONSchema()` (a zod
|
|
2
|
+
* v4 object satisfies this directly) and resolve it to the plain object the
|
|
3
|
+
* wire format (`output_schema`, a JSON string) needs. */
|
|
4
|
+
export function resolveJsonSchema(schema) {
|
|
5
|
+
const withMethod = schema;
|
|
6
|
+
if (typeof withMethod.toJSONSchema === 'function') {
|
|
7
|
+
return schema.toJSONSchema();
|
|
8
|
+
}
|
|
9
|
+
return schema;
|
|
10
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Connection, ConnectionStore } from '../oauth/index.js';
|
|
2
|
+
interface QueryResult {
|
|
3
|
+
rows: Record<string, unknown>[];
|
|
4
|
+
}
|
|
5
|
+
interface QueryClient {
|
|
6
|
+
query(sql: string, values?: unknown[]): Promise<QueryResult>;
|
|
7
|
+
release(): void;
|
|
8
|
+
}
|
|
9
|
+
export interface PostgresPool {
|
|
10
|
+
query(sql: string, values?: unknown[]): Promise<QueryResult>;
|
|
11
|
+
connect(): Promise<QueryClient>;
|
|
12
|
+
}
|
|
13
|
+
export interface PostgresConnectionStoreOptions {
|
|
14
|
+
/** Unquoted PostgreSQL identifier, optionally schema-qualified. Default: connection. */
|
|
15
|
+
table?: string;
|
|
16
|
+
}
|
|
17
|
+
/** A shared PostgreSQL connection store with a transaction-scoped, per-user advisory lock. */
|
|
18
|
+
export declare class PostgresConnectionStore implements ConnectionStore {
|
|
19
|
+
private readonly pool;
|
|
20
|
+
private readonly transaction;
|
|
21
|
+
private readonly table;
|
|
22
|
+
constructor(pool: PostgresPool, options?: PostgresConnectionStoreOptions);
|
|
23
|
+
lock<T>(userId: string, fn: () => Promise<T>): Promise<T>;
|
|
24
|
+
load(userId: string): Promise<Connection | null>;
|
|
25
|
+
save(connection: Connection): Promise<void>;
|
|
26
|
+
/** Delete `userId`'s row only while it still holds `refreshToken`; answers whether one was deleted. Inside
|
|
27
|
+
* `lock` it runs in that transaction, so it commits when the locked callback resolves. */
|
|
28
|
+
remove(userId: string, refreshToken: string): Promise<boolean>;
|
|
29
|
+
/** Connections not saved since `before`, oldest first. Save activity is not necessarily refresh activity. */
|
|
30
|
+
listStale(before: Date): Promise<Connection[]>;
|
|
31
|
+
}
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
function tableName(name) {
|
|
3
|
+
const parts = name.split('.');
|
|
4
|
+
if (parts.length < 1 || parts.length > 2 || parts.some((part) => !/^[a-z_][a-z_0-9]*$/.test(part))) {
|
|
5
|
+
throw new TypeError('table must be a lowercase PostgreSQL identifier, optionally schema-qualified');
|
|
6
|
+
}
|
|
7
|
+
return parts.map((part) => `"${part}"`).join('.');
|
|
8
|
+
}
|
|
9
|
+
function toConnection(row) {
|
|
10
|
+
const expiry = row.access_token_expires_at;
|
|
11
|
+
if (!(expiry instanceof Date) || !Number.isFinite(expiry.getTime())) {
|
|
12
|
+
throw new TypeError('access_token_expires_at must be a PostgreSQL timestamptz (pg Date)');
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
userId: row.user_id,
|
|
16
|
+
runtimeUrl: row.runtime_url,
|
|
17
|
+
refreshToken: row.refresh_token,
|
|
18
|
+
grantId: row.grant_id,
|
|
19
|
+
accessToken: row.access_token,
|
|
20
|
+
accessTokenExpiresAt: expiry.getTime(),
|
|
21
|
+
...(row.id_token == null ? {} : { idToken: row.id_token }),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** A shared PostgreSQL connection store with a transaction-scoped, per-user advisory lock. */
|
|
25
|
+
export class PostgresConnectionStore {
|
|
26
|
+
pool;
|
|
27
|
+
transaction = new AsyncLocalStorage();
|
|
28
|
+
table;
|
|
29
|
+
constructor(pool, options = {}) {
|
|
30
|
+
this.pool = pool;
|
|
31
|
+
this.table = tableName(options.table ?? 'connection');
|
|
32
|
+
}
|
|
33
|
+
async lock(userId, fn) {
|
|
34
|
+
const current = this.transaction.getStore();
|
|
35
|
+
if (current) {
|
|
36
|
+
await current.query('SELECT pg_advisory_xact_lock(hashtextextended($1, 0))', [userId]);
|
|
37
|
+
return fn();
|
|
38
|
+
}
|
|
39
|
+
const db = await this.pool.connect();
|
|
40
|
+
let begun = false;
|
|
41
|
+
try {
|
|
42
|
+
await db.query('BEGIN');
|
|
43
|
+
begun = true;
|
|
44
|
+
await db.query('SELECT pg_advisory_xact_lock(hashtextextended($1, 0))', [userId]);
|
|
45
|
+
const result = await this.transaction.run(db, fn);
|
|
46
|
+
await db.query('COMMIT');
|
|
47
|
+
begun = false;
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (begun) {
|
|
52
|
+
try {
|
|
53
|
+
await db.query('ROLLBACK');
|
|
54
|
+
}
|
|
55
|
+
catch { /* Preserve the original failure. */ }
|
|
56
|
+
}
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
db.release();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async load(userId) {
|
|
64
|
+
const result = await (this.transaction.getStore() ?? this.pool).query(`SELECT user_id, runtime_url, refresh_token, grant_id, access_token, access_token_expires_at, id_token FROM ${this.table} WHERE user_id = $1`, [userId]);
|
|
65
|
+
return result.rows[0] ? toConnection(result.rows[0]) : null;
|
|
66
|
+
}
|
|
67
|
+
async save(connection) {
|
|
68
|
+
const expiry = new Date(connection.accessTokenExpiresAt);
|
|
69
|
+
if (!Number.isFinite(expiry.getTime()))
|
|
70
|
+
throw new TypeError('accessTokenExpiresAt must be a valid timestamp');
|
|
71
|
+
await (this.transaction.getStore() ?? this.pool).query(`INSERT INTO ${this.table} (user_id, runtime_url, refresh_token, grant_id, access_token, access_token_expires_at, id_token, updated_at)
|
|
72
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, clock_timestamp())
|
|
73
|
+
ON CONFLICT (user_id) DO UPDATE SET runtime_url = EXCLUDED.runtime_url, refresh_token = EXCLUDED.refresh_token,
|
|
74
|
+
grant_id = EXCLUDED.grant_id, access_token = EXCLUDED.access_token,
|
|
75
|
+
access_token_expires_at = EXCLUDED.access_token_expires_at, id_token = EXCLUDED.id_token, updated_at = clock_timestamp()`, [connection.userId, connection.runtimeUrl, connection.refreshToken, connection.grantId,
|
|
76
|
+
connection.accessToken, expiry, connection.idToken ?? null]);
|
|
77
|
+
}
|
|
78
|
+
/** Delete `userId`'s row only while it still holds `refreshToken`; answers whether one was deleted. Inside
|
|
79
|
+
* `lock` it runs in that transaction, so it commits when the locked callback resolves. */
|
|
80
|
+
async remove(userId, refreshToken) {
|
|
81
|
+
const result = await (this.transaction.getStore() ?? this.pool).query(`DELETE FROM ${this.table} WHERE user_id = $1 AND refresh_token = $2 RETURNING user_id`, [userId, refreshToken]);
|
|
82
|
+
return result.rows.length > 0;
|
|
83
|
+
}
|
|
84
|
+
/** Connections not saved since `before`, oldest first. Save activity is not necessarily refresh activity. */
|
|
85
|
+
async listStale(before) {
|
|
86
|
+
if (!Number.isFinite(before.getTime()))
|
|
87
|
+
throw new TypeError('before must be a valid Date');
|
|
88
|
+
const result = await (this.transaction.getStore() ?? this.pool).query(`SELECT user_id, runtime_url, refresh_token, grant_id, access_token, access_token_expires_at, id_token
|
|
89
|
+
FROM ${this.table} WHERE updated_at < $1 ORDER BY updated_at ASC, user_id ASC`, [before]);
|
|
90
|
+
return result.rows.map(toConnection);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { ProviderDependency } from '@north-light/api';
|
|
2
|
+
import { Crouter } from '../client.js';
|
|
3
|
+
import { type ConnectionStore } from '../oauth/index.js';
|
|
4
|
+
import { type FakeNotice, type RunNoticeOptions } from './notices.js';
|
|
5
|
+
import { FakeRun } from './run.js';
|
|
6
|
+
import { type FakeRefusalCode, type FakeScope, type FakeUser, type Responder } from './users.js';
|
|
7
|
+
import { type FakeError } from './wire.js';
|
|
8
|
+
/** A runtime operation, named as the daemon's `OPERATIONS` table names it. */
|
|
9
|
+
export type FakeOp = 'grant.get' | 'run.start' | 'run.list' | 'run.get' | 'run.message' | 'run.events' | 'run.interrupt' | 'run.cancel' | 'run.delete' | 'run.rename' | 'question.get' | 'question.answer' | 'trace.get' | 'memory.list' | 'memory.get' | 'provider.tools' | 'provider.call' | 'profile.list';
|
|
10
|
+
/** A directory operation. */
|
|
11
|
+
export type FakeDirectoryOp = 'directory.discovery' | 'directory.token' | 'directory.jwks';
|
|
12
|
+
/** What `intercept` / `failNext` / `gate` match: a runtime operation, `'*'` (every runtime operation), or a directory operation. */
|
|
13
|
+
export type FakeInterceptOp = FakeOp | '*' | FakeDirectoryOp;
|
|
14
|
+
/** One runtime request the fake received. */
|
|
15
|
+
export interface FakeCall {
|
|
16
|
+
/** The bearer token's `sub` when it names a person this cloud knows (whatever their grant or refresh-token state);
|
|
17
|
+
* null when there is no bearer, it does not decode, or its `sub` is a person the cloud has never seen. */
|
|
18
|
+
userId: string | null;
|
|
19
|
+
op: FakeOp;
|
|
20
|
+
/** The run the call addresses: the id in the path of `run.get`/`message`/`events`/`interrupt`/`cancel`/`delete`/
|
|
21
|
+
* `rename` and `trace.get` (as sent, whether or not such a run exists); for `question.get`/`answer`, the run that
|
|
22
|
+
* holds the question (null when no run of the caller does). Null for every other operation. */
|
|
23
|
+
runId: string | null;
|
|
24
|
+
/** The question id in the path of `question.get`/`answer`; null otherwise. */
|
|
25
|
+
questionId: string | null;
|
|
26
|
+
method: string;
|
|
27
|
+
path: string;
|
|
28
|
+
query: Record<string, string>;
|
|
29
|
+
/** Parsed JSON body, or null. */
|
|
30
|
+
body: unknown;
|
|
31
|
+
idempotencyKey: string | null;
|
|
32
|
+
/** The status answered; null while held or before an answer. */
|
|
33
|
+
status: number | null;
|
|
34
|
+
at: number;
|
|
35
|
+
}
|
|
36
|
+
/** One directory request the fake received. */
|
|
37
|
+
export interface FakeDirectoryCall {
|
|
38
|
+
op: FakeDirectoryOp;
|
|
39
|
+
/** The refresh token's owner on `directory.token`; null otherwise. */
|
|
40
|
+
userId: string | null;
|
|
41
|
+
/** `ok`, a refusal code, an HTTP status, or `unreachable`. */
|
|
42
|
+
answer: 'ok' | FakeRefusalCode | number | 'unreachable';
|
|
43
|
+
at: number;
|
|
44
|
+
}
|
|
45
|
+
/** A scripted failure: `'unreachable'` (fetch rejects), an HTTP status, or a `FakeCloud.error(...)` envelope (runtime only). */
|
|
46
|
+
export type FakeAnswer = 'unreachable' | number | FakeError;
|
|
47
|
+
/** Runs before an operation. It may await (a gate), act, or return a replacement answer. */
|
|
48
|
+
export type Intercept = (call: FakeCall | FakeDirectoryCall) => void | FakeAnswer | Promise<void | FakeAnswer>;
|
|
49
|
+
/** A provider tool's handler; its return value is the call's result. Throw `FakeCloud.error(...)` for an envelope. */
|
|
50
|
+
export type FakeToolHandler = (input: {
|
|
51
|
+
userId: string;
|
|
52
|
+
arguments: Record<string, unknown>;
|
|
53
|
+
signal: AbortSignal | null;
|
|
54
|
+
}) => unknown;
|
|
55
|
+
/** `providers.define` input. */
|
|
56
|
+
export interface FakeProvider {
|
|
57
|
+
displayName?: string;
|
|
58
|
+
/** `available` (default), or an install state: listed with `error`, calls answer 503 `provider_unavailable`. */
|
|
59
|
+
status?: 'available' | 'installing' | 'install_failed';
|
|
60
|
+
error?: string;
|
|
61
|
+
tools: Record<string, {
|
|
62
|
+
group?: string;
|
|
63
|
+
description?: string;
|
|
64
|
+
handler: FakeToolHandler;
|
|
65
|
+
}>;
|
|
66
|
+
}
|
|
67
|
+
export interface FakeCloudOptions {
|
|
68
|
+
/** The app registration this cloud serves. */
|
|
69
|
+
clientId: string;
|
|
70
|
+
/** Default `https://directory.test`. */
|
|
71
|
+
issuer?: string;
|
|
72
|
+
/** Default `https://runtime.test`; a function gives each user their own host. */
|
|
73
|
+
runtimeUrl?: string | ((userId: string) => string);
|
|
74
|
+
/** The runtime's identity in run-notice tokens (`act.sub`). Default `runtime:test`. */
|
|
75
|
+
runtimeId?: string;
|
|
76
|
+
/** The runtime's client id in run-notice tokens (`client_id`). Default `runtime-client`. */
|
|
77
|
+
runtimeClientId?: string;
|
|
78
|
+
/** The runtime's release; default `SDK_VERSION`. An older one refuses this SDK with 426. */
|
|
79
|
+
runtimeVersion?: string;
|
|
80
|
+
/** Idle close of a run's event stream. Default 60 000 ms. */
|
|
81
|
+
streamIdleMs?: number;
|
|
82
|
+
/** Discovery's `account_app_uri_template`; default `<issuer>/account/apps/{client_id}`, null omits it. */
|
|
83
|
+
accountAppUriTemplate?: string | null;
|
|
84
|
+
/** Runtime→app run notices; without it nothing is sent. */
|
|
85
|
+
runNotice?: RunNoticeOptions;
|
|
86
|
+
/**
|
|
87
|
+
* Whether the runtime can start an `isolated: true` run. `available` (default) models a crouter cloud guest, where
|
|
88
|
+
* the Linux root launcher always exists: the run starts, `run.params.isolated` is true, and a `cwd` outside the run's
|
|
89
|
+
* root folder (`/apps/<clientId>/nodes/<run id>`) is 400 `invalid_request` `param: cwd`. `unavailable` models a
|
|
90
|
+
* runtime without the launcher (a developer's Mac): every isolated start is 503 `isolated_unavailable`, retryable.
|
|
91
|
+
*/
|
|
92
|
+
isolatedRuns?: 'available' | 'unavailable';
|
|
93
|
+
}
|
|
94
|
+
/** `cloud.runs` and `user.runs`: the runs in start order, and the responder for them. */
|
|
95
|
+
export interface FakeRuns {
|
|
96
|
+
/** The runs in start order (deleted ones included, with `deleted` set). */
|
|
97
|
+
readonly all: FakeRun[];
|
|
98
|
+
/** Set the responder (null clears it). On a user's `runs` it overrides the cloud's for that user. */
|
|
99
|
+
respond(fn: Responder | null): void;
|
|
100
|
+
}
|
|
101
|
+
/** A fake crouter cloud: the directory (discovery, refresh, JWKS) and each person's runtime, behind `fetch`. */
|
|
102
|
+
export declare class FakeCloud {
|
|
103
|
+
/** Pass to `OAuth2Client` (and `verifyRunNotice`) as `fetch`. Unique per cloud. */
|
|
104
|
+
readonly fetch: typeof fetch;
|
|
105
|
+
readonly issuer: string;
|
|
106
|
+
readonly clientId: string;
|
|
107
|
+
/** Every runtime request, in order. */
|
|
108
|
+
readonly calls: FakeCall[];
|
|
109
|
+
/** Every directory request, in order. */
|
|
110
|
+
readonly directoryCalls: FakeDirectoryCall[];
|
|
111
|
+
/** Every run-notice attempt, in order. */
|
|
112
|
+
readonly notices: FakeNotice[];
|
|
113
|
+
/** What responders threw. */
|
|
114
|
+
readonly responderErrors: unknown[];
|
|
115
|
+
readonly runs: FakeRuns;
|
|
116
|
+
readonly providers: {
|
|
117
|
+
define(provider: string, definition: FakeProvider): void;
|
|
118
|
+
};
|
|
119
|
+
private readonly options;
|
|
120
|
+
private readonly users;
|
|
121
|
+
private readonly handles;
|
|
122
|
+
private readonly allRuns;
|
|
123
|
+
private readonly providerDefs;
|
|
124
|
+
private intercepts;
|
|
125
|
+
private readonly gates;
|
|
126
|
+
private responder;
|
|
127
|
+
private readonly key;
|
|
128
|
+
private readonly runNotices;
|
|
129
|
+
private readonly host;
|
|
130
|
+
/** Superseded refresh tokens → owner, for `refresh_token_reused`. */
|
|
131
|
+
private readonly superseded;
|
|
132
|
+
private tokenSerial;
|
|
133
|
+
private refreshSerial;
|
|
134
|
+
private runSerial;
|
|
135
|
+
constructor(options: FakeCloudOptions);
|
|
136
|
+
/** A `FakeError` envelope for intercepts, `failNext` and provider handlers. `retryable` defaults to `status === 503`. */
|
|
137
|
+
static error(input: {
|
|
138
|
+
status: number;
|
|
139
|
+
code: string;
|
|
140
|
+
message?: string;
|
|
141
|
+
origin?: string;
|
|
142
|
+
retryable?: boolean;
|
|
143
|
+
details?: unknown;
|
|
144
|
+
param?: string;
|
|
145
|
+
}): FakeError;
|
|
146
|
+
/** The `directory.token` subset of `directoryCalls`. */
|
|
147
|
+
get refreshes(): FakeDirectoryCall[];
|
|
148
|
+
/** That user's handle, created on first use. */
|
|
149
|
+
user(userId: string): FakeUser;
|
|
150
|
+
/**
|
|
151
|
+
* Make `userId`'s grant active and save a stored connection for them in `store`, with its access token expired so
|
|
152
|
+
* the first call refreshes through the directory. Default scopes: `crtr:llm`, `crtr:act`.
|
|
153
|
+
*/
|
|
154
|
+
connect(store: ConnectionStore, userId: string, options?: {
|
|
155
|
+
scopes?: FakeScope[];
|
|
156
|
+
dependencies?: Record<string, ProviderDependency>;
|
|
157
|
+
}): Promise<FakeUser>;
|
|
158
|
+
/**
|
|
159
|
+
* A real `Crouter` on `userId`'s runtime with a fresh access token and no refresh: refusals never reach it. For
|
|
160
|
+
* code whose production signature takes a `Crouter`; an app that goes through `OAuth2Client` tests through it.
|
|
161
|
+
*/
|
|
162
|
+
crouter(userId: string): Crouter;
|
|
163
|
+
/** Run `fn` before `op`. `times` defaults to Infinity. `user` filters runtime operations and `directory.token`. */
|
|
164
|
+
intercept(op: FakeInterceptOp, fn: Intercept, options?: {
|
|
165
|
+
user?: string;
|
|
166
|
+
times?: number;
|
|
167
|
+
}): () => void;
|
|
168
|
+
/** Answer the next `times` (default 1) matching requests with `answer`. */
|
|
169
|
+
failNext(op: FakeInterceptOp, answer: FakeAnswer, options?: {
|
|
170
|
+
user?: string;
|
|
171
|
+
times?: number;
|
|
172
|
+
}): void;
|
|
173
|
+
/** Hold every matching request until `release()`, which frees them all and removes the gate. */
|
|
174
|
+
gate(op: FakeInterceptOp, options?: {
|
|
175
|
+
user?: string;
|
|
176
|
+
}): () => void;
|
|
177
|
+
/** Clear runs, calls, intercepts (gates are released), responders, memory, idempotency keys and notices. Grants
|
|
178
|
+
* and refresh-token chains stay, so stored connections remain valid. */
|
|
179
|
+
reset(): void;
|
|
180
|
+
/** The exact error the SDK raises when the directory refuses a refresh with `code` (`refusalOf(e)` is set). */
|
|
181
|
+
refusalError(code: FakeRefusalCode): Promise<unknown>;
|
|
182
|
+
private state;
|
|
183
|
+
private runtimeUrlFor;
|
|
184
|
+
private isRuntimeOrigin;
|
|
185
|
+
private headers;
|
|
186
|
+
private route;
|
|
187
|
+
private runIntercepts;
|
|
188
|
+
private directory;
|
|
189
|
+
private refresh;
|
|
190
|
+
private newRefreshToken;
|
|
191
|
+
private accessToken;
|
|
192
|
+
private runtime;
|
|
193
|
+
private ownedRun;
|
|
194
|
+
private respond;
|
|
195
|
+
private idempotencyKey;
|
|
196
|
+
private handle;
|
|
197
|
+
private grantGet;
|
|
198
|
+
private runStart;
|
|
199
|
+
/** An isolated run's root folder on a guest (`appSpace(app).nodes/<run id>`, apps root `/apps`). */
|
|
200
|
+
private isolatedRoot;
|
|
201
|
+
private runMessage;
|
|
202
|
+
private question;
|
|
203
|
+
private memoryStore;
|
|
204
|
+
private memoryVisible;
|
|
205
|
+
private memoryList;
|
|
206
|
+
private providerEntry;
|
|
207
|
+
private providerTools;
|
|
208
|
+
private providerCall;
|
|
209
|
+
}
|