@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,46 @@
|
|
|
1
|
+
import { calculateJwkThumbprint, exportJWK, generateKeyPair as generateSigningKeyPair } from 'jose';
|
|
2
|
+
import { CrouterError } from '../errors.js';
|
|
3
|
+
/** Make an app signing key and the public JWK to register in the directory. */
|
|
4
|
+
export async function generateKeyPair() {
|
|
5
|
+
const keys = await generateSigningKeyPair('ES256', { extractable: true });
|
|
6
|
+
const publicKey = await exportJWK(keys.publicKey);
|
|
7
|
+
const kid = await calculateJwkThumbprint(publicKey, 'sha256');
|
|
8
|
+
const properties = { kid, alg: 'ES256', use: 'sig' };
|
|
9
|
+
return {
|
|
10
|
+
privateJwk: { ...await exportJWK(keys.privateKey), ...properties },
|
|
11
|
+
publicJwk: { ...publicKey, ...properties },
|
|
12
|
+
kid,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const keygenHint = 'make one with: pnpm exec crouter-sdk keygen';
|
|
16
|
+
/** Check that `value` (a JWK object or its JSON text) is a private signing key with a `kid`, and return it as
|
|
17
|
+
* `OAuth2Options.privateKey` takes it. `source` names where it came from in the error, e.g. a file path. */
|
|
18
|
+
export function parsePrivateJwk(value, source = 'The key') {
|
|
19
|
+
let key = value;
|
|
20
|
+
if (typeof value === 'string') {
|
|
21
|
+
try {
|
|
22
|
+
key = JSON.parse(value);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new CrouterError(`${source} is not JSON; it must hold a private JWK with a kid (${keygenHint}).`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (!key || typeof key !== 'object' || Array.isArray(key) || typeof key.d !== 'string' || !key.d
|
|
29
|
+
|| typeof key.kid !== 'string' || !key.kid) {
|
|
30
|
+
throw new CrouterError(`${source} must hold a private JWK with a kid (${keygenHint}).`);
|
|
31
|
+
}
|
|
32
|
+
return key;
|
|
33
|
+
}
|
|
34
|
+
/** Read a private JWK file, such as the one `crouter-sdk keygen` writes, and check it as `parsePrivateJwk` does. Node only. */
|
|
35
|
+
export async function privateKeyFromFile(path) {
|
|
36
|
+
const load = new Function('specifier', 'return import(specifier)');
|
|
37
|
+
const { readFile } = await load('node:fs/promises');
|
|
38
|
+
let text;
|
|
39
|
+
try {
|
|
40
|
+
text = await readFile(path, 'utf8');
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw new CrouterError(`${path} could not be read (${error instanceof Error ? error.message : String(error)}); it must hold a private JWK with a kid (${keygenHint}).`);
|
|
44
|
+
}
|
|
45
|
+
return parsePrivateJwk(text, path);
|
|
46
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { APIError } from '../errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* What a refused refresh means for the app's data:
|
|
4
|
+
* - `removed` (`grant_removed`): the person removed the app or deleted their account. The runtime has
|
|
5
|
+
* deleted the app's runs; delete what the app stores for them.
|
|
6
|
+
* - `paused` (`grant_suspended`): the grant is suspended or expired. Nothing was deleted; keep the
|
|
7
|
+
* person's data and show the app paused until they sign in again.
|
|
8
|
+
* - `reconnect` (`refresh_token_expired`, `refresh_token_reused`, or `grant_revoked` with no reason given):
|
|
9
|
+
* the grant still exists. Nothing was deleted; ask them to sign in again.
|
|
10
|
+
*/
|
|
11
|
+
export type RefusalKind = 'removed' | 'paused' | 'reconnect';
|
|
12
|
+
export declare const isRefusalCode: (code: string) => boolean;
|
|
13
|
+
/** A connection the directory refused to refresh: passed to `OAuth2Options.onRefusal`, and read from the
|
|
14
|
+
* refused call's error with `refusalOf(error)`. The SDK removes the stored connection once the handler resolves. */
|
|
15
|
+
export interface Refusal {
|
|
16
|
+
kind: RefusalKind;
|
|
17
|
+
/** The directory's code: `grant_removed`, `grant_suspended`, `refresh_token_expired`, `refresh_token_reused`, or `grant_revoked`. */
|
|
18
|
+
code: string;
|
|
19
|
+
/** The connection's crouter cloud user id (`user:<id>`). */
|
|
20
|
+
userId: string;
|
|
21
|
+
/** The grant the refused connection held. */
|
|
22
|
+
grantId: string;
|
|
23
|
+
/** The `AuthenticationError` (401, `origin: 'directory'`, `userAction: 'reconnect'`) the refusal raised. */
|
|
24
|
+
error: APIError;
|
|
25
|
+
}
|
|
26
|
+
export declare function refusalKindOf(code: string): RefusalKind | null;
|
|
27
|
+
export declare function attachRefusal(error: APIError, refusal: Refusal): void;
|
|
28
|
+
/** The `Refusal` a call failed with when the directory refused its connection's refresh, else `null`. Read it to
|
|
29
|
+
* answer one request (show "sign in again"); act on the connection itself in `OAuth2Options.onRefusal`, which the
|
|
30
|
+
* SDK has already run. Only an error raised by `OAuth2Client` refreshing a stored connection carries one; a 429,
|
|
31
|
+
* `cap_exhausted`, `scope_missing`, or a runtime-origin `grant_revoked` does not. */
|
|
32
|
+
export declare function refusalOf(error: unknown): Refusal | null;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { APIError } from '../errors.js';
|
|
2
|
+
/** The one map from a directory refresh refusal code to what it means. Every other use of these codes reads its keys. */
|
|
3
|
+
const refusalKinds = {
|
|
4
|
+
grant_removed: 'removed', grant_suspended: 'paused',
|
|
5
|
+
refresh_token_expired: 'reconnect', refresh_token_reused: 'reconnect', grant_revoked: 'reconnect',
|
|
6
|
+
};
|
|
7
|
+
export const isRefusalCode = (code) => Object.hasOwn(refusalKinds, code);
|
|
8
|
+
const refusalKey = Symbol('crouter.refusal');
|
|
9
|
+
export function refusalKindOf(code) { return isRefusalCode(code) ? refusalKinds[code] : null; }
|
|
10
|
+
export function attachRefusal(error, refusal) { Object.defineProperty(error, refusalKey, { value: refusal }); }
|
|
11
|
+
/** The `Refusal` a call failed with when the directory refused its connection's refresh, else `null`. Read it to
|
|
12
|
+
* answer one request (show "sign in again"); act on the connection itself in `OAuth2Options.onRefusal`, which the
|
|
13
|
+
* SDK has already run. Only an error raised by `OAuth2Client` refreshing a stored connection carries one; a 429,
|
|
14
|
+
* `cap_exhausted`, `scope_missing`, or a runtime-origin `grant_revoked` does not. */
|
|
15
|
+
export function refusalOf(error) {
|
|
16
|
+
return error instanceof APIError ? error[refusalKey] ?? null : null;
|
|
17
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { NodeStreamEvent } from './node-stream.js';
|
|
2
|
+
/** One tool call rendered as an activity step. */
|
|
3
|
+
export interface ActivityStep {
|
|
4
|
+
/** The tool call's id, stable across its status changes. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** Tool name, e.g. `bash` or `read`. */
|
|
7
|
+
tool: string;
|
|
8
|
+
/** The daemon's one-line summary of the call. */
|
|
9
|
+
summary: string;
|
|
10
|
+
/** What `describe` returned for this call, for display. */
|
|
11
|
+
label: string;
|
|
12
|
+
/** Where the call stands. */
|
|
13
|
+
status: 'running' | 'done' | 'failed';
|
|
14
|
+
}
|
|
15
|
+
/** Turn a streamed tool call into a label, or return null to omit it. */
|
|
16
|
+
export type DescribeTool = (tool: string, summary: string) => string | null;
|
|
17
|
+
/** Plain labels for the tools every node can use. */
|
|
18
|
+
export declare const describeToolDefault: DescribeTool;
|
|
19
|
+
/** Yield an immutable activity snapshot after every streamed tool-call change. */
|
|
20
|
+
export declare function followActivity(stream: AsyncIterable<NodeStreamEvent>, options?: {
|
|
21
|
+
describe?: DescribeTool;
|
|
22
|
+
}): AsyncGenerator<ActivityStep[]>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Plain labels for the tools every node can use. */
|
|
2
|
+
export const describeToolDefault = (tool) => {
|
|
3
|
+
switch (tool) {
|
|
4
|
+
case 'bash': return 'running bash';
|
|
5
|
+
case 'read': return 'reading';
|
|
6
|
+
case 'write': return 'writing';
|
|
7
|
+
case 'edit': return 'editing';
|
|
8
|
+
default: return tool;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
/** Yield an immutable activity snapshot after every streamed tool-call change. */
|
|
12
|
+
export async function* followActivity(stream, options = {}) {
|
|
13
|
+
const describe = options.describe ?? describeToolDefault;
|
|
14
|
+
const steps = [];
|
|
15
|
+
const byId = new Map();
|
|
16
|
+
for await (const event of stream) {
|
|
17
|
+
if (event.type === 'node.tool_call.started') {
|
|
18
|
+
const label = describe(event.tool, event.summary);
|
|
19
|
+
if (label === null || byId.has(event.tool_call_id))
|
|
20
|
+
continue;
|
|
21
|
+
const step = {
|
|
22
|
+
id: event.tool_call_id,
|
|
23
|
+
tool: event.tool,
|
|
24
|
+
summary: event.summary,
|
|
25
|
+
label,
|
|
26
|
+
status: 'running',
|
|
27
|
+
};
|
|
28
|
+
byId.set(step.id, step);
|
|
29
|
+
steps.push(step);
|
|
30
|
+
yield snapshot(steps);
|
|
31
|
+
}
|
|
32
|
+
else if (event.type === 'node.tool_call.completed') {
|
|
33
|
+
const step = byId.get(event.tool_call_id);
|
|
34
|
+
if (step === undefined || step.status !== 'running')
|
|
35
|
+
continue;
|
|
36
|
+
step.status = event.status === 'ok' ? 'done' : 'failed';
|
|
37
|
+
yield snapshot(steps);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function snapshot(steps) {
|
|
42
|
+
return steps.map((step) => ({ ...step }));
|
|
43
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RequestOptions } from '../types.js';
|
|
2
|
+
import type { Request } from './request.js';
|
|
3
|
+
/** A short-lived signed link to an uploaded file's stored bytes. */
|
|
4
|
+
export interface AttachmentLink {
|
|
5
|
+
url: string;
|
|
6
|
+
expires_at: string;
|
|
7
|
+
}
|
|
8
|
+
/** Links to files a run received as attachments (app listener only). */
|
|
9
|
+
export declare class Attachments {
|
|
10
|
+
private readonly request;
|
|
11
|
+
constructor(request: Request);
|
|
12
|
+
/** A signed download link for the upload fetched to `path`, an absolute path under a run's
|
|
13
|
+
* `attachments/` folder. A path with no recorded upload, or in a run the caller cannot read, is `not_found`. */
|
|
14
|
+
link(path: string, options?: RequestOptions): Promise<AttachmentLink>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { withQuery } from './request.js';
|
|
2
|
+
/** Links to files a run received as attachments (app listener only). */
|
|
3
|
+
export class Attachments {
|
|
4
|
+
request;
|
|
5
|
+
constructor(request) {
|
|
6
|
+
this.request = request;
|
|
7
|
+
}
|
|
8
|
+
/** A signed download link for the upload fetched to `path`, an absolute path under a run's
|
|
9
|
+
* `attachments/` folder. A path with no recorded upload, or in a run the caller cannot read, is `not_found`. */
|
|
10
|
+
link(path, options) {
|
|
11
|
+
return this.request('GET', withQuery('/v1/attachments/link', { path }), undefined, options);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type BashRunDTO, type BashRunParams } from '@north-light/api';
|
|
2
|
+
import type { RequestOptions } from '../types.js';
|
|
3
|
+
import type { Request } from './request.js';
|
|
4
|
+
export declare class Bash {
|
|
5
|
+
private readonly request;
|
|
6
|
+
private readonly defaultTimeout;
|
|
7
|
+
constructor(request: Request, defaultTimeout: number);
|
|
8
|
+
run(params: BashRunParams, options?: RequestOptions): Promise<BashRunDTO>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { routes } from '@north-light/api';
|
|
2
|
+
const DEFAULT_TIMEOUT_SECONDS = 60;
|
|
3
|
+
const TIMEOUT_ALLOWANCE_MS = 15_000;
|
|
4
|
+
export class Bash {
|
|
5
|
+
request;
|
|
6
|
+
defaultTimeout;
|
|
7
|
+
constructor(request, defaultTimeout) {
|
|
8
|
+
this.request = request;
|
|
9
|
+
this.defaultTimeout = defaultTimeout;
|
|
10
|
+
}
|
|
11
|
+
run(params, options) {
|
|
12
|
+
const requiredTimeout = (params.timeout_s ?? DEFAULT_TIMEOUT_SECONDS) * 1000 + TIMEOUT_ALLOWANCE_MS;
|
|
13
|
+
return this.request('POST', routes.bash(), params, { ...options, timeout: Math.max(options?.timeout ?? this.defaultTimeout, requiredTimeout) });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type HistoryGrepQuery, type HistoryGrepResultDTO, type HistoryReadQuery, type HistoryReadResultDTO, type HistorySearchQuery, type HistorySearchResultDTO, type HistoryStatsQuery, type HistoryStatsResultDTO } from '@north-light/api';
|
|
2
|
+
import type { RequestOptions } from '../../types.js';
|
|
3
|
+
import type { Request } from '../request.js';
|
|
4
|
+
export declare class CanvasHistory {
|
|
5
|
+
private readonly request;
|
|
6
|
+
constructor(request: Request);
|
|
7
|
+
search(body: HistorySearchQuery, options?: RequestOptions): Promise<HistorySearchResultDTO>;
|
|
8
|
+
grep(body: HistoryGrepQuery, options?: RequestOptions): Promise<HistoryGrepResultDTO>;
|
|
9
|
+
read(query: HistoryReadQuery, options?: RequestOptions): Promise<HistoryReadResultDTO>;
|
|
10
|
+
stats(body: HistoryStatsQuery, options?: RequestOptions): Promise<HistoryStatsResultDTO>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { routes, } from '@north-light/api';
|
|
2
|
+
import { withQuery } from '../request.js';
|
|
3
|
+
export class CanvasHistory {
|
|
4
|
+
request;
|
|
5
|
+
constructor(request) {
|
|
6
|
+
this.request = request;
|
|
7
|
+
}
|
|
8
|
+
search(body, options) {
|
|
9
|
+
return this.request('POST', routes.canvasHistorySearch(), body, options);
|
|
10
|
+
}
|
|
11
|
+
grep(body, options) {
|
|
12
|
+
return this.request('POST', routes.canvasHistoryGrep(), body, options);
|
|
13
|
+
}
|
|
14
|
+
read(query, options) {
|
|
15
|
+
return this.request('GET', withQuery(routes.canvasHistoryRead(), query), undefined, options);
|
|
16
|
+
}
|
|
17
|
+
stats(body, options) {
|
|
18
|
+
return this.request('POST', routes.canvasHistoryStats(), body, options);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type AttentionCountsDTO, type AttentionCountsRequest, type AttentionDTO, type DashboardDTO, type DashboardQuery, type PruneRequest, type PruneResultDTO, type RosterDTO, type SnapshotDTO, type WatchRequest, type WatchResultDTO, type UnwatchQuery, type UnwatchResultDTO } from '@north-light/api';
|
|
2
|
+
import type { RequestOptions } from '../types.js';
|
|
3
|
+
import { CanvasHistory } from './canvas/history.js';
|
|
4
|
+
import type { Request } from './request.js';
|
|
5
|
+
export declare class Canvas {
|
|
6
|
+
private readonly request;
|
|
7
|
+
readonly history: CanvasHistory;
|
|
8
|
+
constructor(request: Request);
|
|
9
|
+
attention(options?: RequestOptions): Promise<AttentionDTO>;
|
|
10
|
+
attentionCounts(body: AttentionCountsRequest, options?: RequestOptions): Promise<AttentionCountsDTO>;
|
|
11
|
+
snapshot(options?: RequestOptions): Promise<SnapshotDTO>;
|
|
12
|
+
roster(options?: RequestOptions): Promise<RosterDTO>;
|
|
13
|
+
dashboard(query?: DashboardQuery, options?: RequestOptions): Promise<DashboardDTO>;
|
|
14
|
+
/** Watch an object; `watcher` makes a custom object the receiver instead of the caller's node. */
|
|
15
|
+
watch(ref: string, body?: WatchRequest, options?: RequestOptions): Promise<WatchResultDTO>;
|
|
16
|
+
/** Remove a watch. `watcher` identifies the custom object whose watch is removed. */
|
|
17
|
+
unwatch(ref: string, query?: UnwatchQuery, options?: RequestOptions): Promise<UnwatchResultDTO>;
|
|
18
|
+
prune(body: PruneRequest, options?: RequestOptions): Promise<PruneResultDTO>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { routes, } from '@north-light/api';
|
|
2
|
+
import { CanvasHistory } from './canvas/history.js';
|
|
3
|
+
import { withQuery } from './request.js';
|
|
4
|
+
export class Canvas {
|
|
5
|
+
request;
|
|
6
|
+
history;
|
|
7
|
+
constructor(request) {
|
|
8
|
+
this.request = request;
|
|
9
|
+
this.history = new CanvasHistory(request);
|
|
10
|
+
}
|
|
11
|
+
attention(options) {
|
|
12
|
+
return this.request('GET', routes.canvasAttention(), undefined, options);
|
|
13
|
+
}
|
|
14
|
+
attentionCounts(body, options) {
|
|
15
|
+
return this.request('POST', routes.canvasAttentionCounts(), body, options);
|
|
16
|
+
}
|
|
17
|
+
snapshot(options) {
|
|
18
|
+
return this.request('GET', routes.canvasSnapshot(), undefined, options);
|
|
19
|
+
}
|
|
20
|
+
roster(options) {
|
|
21
|
+
return this.request('GET', routes.canvasRoster(), undefined, options);
|
|
22
|
+
}
|
|
23
|
+
async dashboard(query, options) {
|
|
24
|
+
const nodeQuery = query?.under === undefined ? undefined : { under: query.under };
|
|
25
|
+
const [nodes, status] = await Promise.all([
|
|
26
|
+
this.request('GET', withQuery(routes.nodes(), nodeQuery), undefined, options),
|
|
27
|
+
this.request('GET', routes.status(), undefined, options),
|
|
28
|
+
]);
|
|
29
|
+
return { nodes, counts: status.node_counts, generated_at: new Date().toISOString() };
|
|
30
|
+
}
|
|
31
|
+
/** Watch an object; `watcher` makes a custom object the receiver instead of the caller's node. */
|
|
32
|
+
watch(ref, body = {}, options) {
|
|
33
|
+
return this.request('POST', routes.objectWatch(ref), body, options);
|
|
34
|
+
}
|
|
35
|
+
/** Remove a watch. `watcher` identifies the custom object whose watch is removed. */
|
|
36
|
+
unwatch(ref, query, options) {
|
|
37
|
+
return this.request('DELETE', withQuery(routes.objectWatch(ref), query), undefined, options);
|
|
38
|
+
}
|
|
39
|
+
prune(body, options) {
|
|
40
|
+
return this.request('POST', routes.canvasPrune(), body, options);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ArmCronRequest, type CancelCronQuery, type CronDTO, type CronRunDTO, type CronScopeQuery, type CronShowDTO, type ListCronsQuery, type PokeCronsResult } from '@north-light/api';
|
|
2
|
+
import type { RequestOptions } from '../types.js';
|
|
3
|
+
import type { Request } from './request.js';
|
|
4
|
+
export declare class Crons {
|
|
5
|
+
private readonly request;
|
|
6
|
+
constructor(request: Request);
|
|
7
|
+
create(body: ArmCronRequest, options?: RequestOptions): Promise<CronDTO>;
|
|
8
|
+
retrieve(id: string, query?: CronScopeQuery, options?: RequestOptions): Promise<CronShowDTO>;
|
|
9
|
+
list(query?: ListCronsQuery, options?: RequestOptions): Promise<CronDTO[]>;
|
|
10
|
+
pause(id: string, query?: CronScopeQuery, options?: RequestOptions): Promise<CronDTO>;
|
|
11
|
+
resume(id: string, query?: CronScopeQuery, options?: RequestOptions): Promise<CronDTO>;
|
|
12
|
+
run(id: string, query?: CronScopeQuery, options?: RequestOptions): Promise<CronRunDTO>;
|
|
13
|
+
delete(id: string, query?: CancelCronQuery, options?: RequestOptions): Promise<void>;
|
|
14
|
+
poke(options?: RequestOptions): Promise<PokeCronsResult>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { routes, } from '@north-light/api';
|
|
2
|
+
import { cronId } from './identifiers.js';
|
|
3
|
+
import { withQuery } from './request.js';
|
|
4
|
+
export class Crons {
|
|
5
|
+
request;
|
|
6
|
+
constructor(request) {
|
|
7
|
+
this.request = request;
|
|
8
|
+
}
|
|
9
|
+
create(body, options) {
|
|
10
|
+
return this.request('POST', routes.crons(), body, options);
|
|
11
|
+
}
|
|
12
|
+
async retrieve(id, query, options) {
|
|
13
|
+
return this.request('GET', withQuery(routes.cron(cronId(id)), query), undefined, options);
|
|
14
|
+
}
|
|
15
|
+
list(query, options) {
|
|
16
|
+
return this.request('GET', withQuery(routes.crons(), query), undefined, options);
|
|
17
|
+
}
|
|
18
|
+
async pause(id, query, options) {
|
|
19
|
+
return this.request('POST', withQuery(routes.cronPause(cronId(id)), query), {}, options);
|
|
20
|
+
}
|
|
21
|
+
async resume(id, query, options) {
|
|
22
|
+
return this.request('POST', withQuery(routes.cronResume(cronId(id)), query), {}, options);
|
|
23
|
+
}
|
|
24
|
+
async run(id, query, options) {
|
|
25
|
+
return this.request('POST', withQuery(routes.cronRun(cronId(id)), query), {}, options);
|
|
26
|
+
}
|
|
27
|
+
async delete(id, query, options) {
|
|
28
|
+
return this.request('DELETE', withQuery(routes.cron(cronId(id)), query), undefined, options);
|
|
29
|
+
}
|
|
30
|
+
poke(options) {
|
|
31
|
+
return this.request('POST', routes.cronsPoke(), {}, options);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type AckCustomDeliveriesResponseDTO, type PublishCustomObjectRequestDTO, type PublishCustomObjectResponseDTO, type QueueLineDTO, type RegisterCustomObjectRequestDTO, type RegisterCustomObjectResponseDTO } from '@north-light/api';
|
|
2
|
+
import type { RequestOptions } from '../types.js';
|
|
3
|
+
import type { Request } from './request.js';
|
|
4
|
+
/** Queue streams replay every pending delivery on connect. `after` is accepted for compatibility but ignored for pending work. */
|
|
5
|
+
export type ReceiveOptions = Pick<RequestOptions, 'signal' | 'headers' | 'maxRetries'> & {
|
|
6
|
+
after?: number;
|
|
7
|
+
};
|
|
8
|
+
/** A single queue connection. EOF (including rotate/unregister/receiver change) ends iteration; reconnect to replay unacknowledged work. */
|
|
9
|
+
export interface CustomDeliveryStream extends AsyncIterableIterator<QueueLineDTO> {
|
|
10
|
+
}
|
|
11
|
+
export declare class CustomObjects {
|
|
12
|
+
private readonly request;
|
|
13
|
+
private readonly open;
|
|
14
|
+
constructor(request: Request, open: (path: string, options: ReceiveOptions) => Promise<Response>);
|
|
15
|
+
register(body: RegisterCustomObjectRequestDTO, options?: RequestOptions): Promise<RegisterCustomObjectResponseDTO>;
|
|
16
|
+
publish(ref: string, body: PublishCustomObjectRequestDTO, options?: RequestOptions): Promise<PublishCustomObjectResponseDTO>;
|
|
17
|
+
unregister(ref: string, options?: RequestOptions): Promise<void>;
|
|
18
|
+
ack(ref: string, deliveryIds: string[], options?: RequestOptions): Promise<AckCustomDeliveriesResponseDTO>;
|
|
19
|
+
receive(ref: string, options?: ReceiveOptions): CustomDeliveryStream;
|
|
20
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { routes, } from '@north-light/api';
|
|
2
|
+
import { APIConnectionError, APIError, APIUserAbortError } from '../errors.js';
|
|
3
|
+
import { withQuery } from './request.js';
|
|
4
|
+
export class CustomObjects {
|
|
5
|
+
request;
|
|
6
|
+
open;
|
|
7
|
+
constructor(request, open) {
|
|
8
|
+
this.request = request;
|
|
9
|
+
this.open = open;
|
|
10
|
+
}
|
|
11
|
+
register(body, options) {
|
|
12
|
+
return this.request('POST', routes.customObjects(), body, options);
|
|
13
|
+
}
|
|
14
|
+
publish(ref, body, options) {
|
|
15
|
+
return this.request('POST', routes.customObjectEvents(ref), body, options);
|
|
16
|
+
}
|
|
17
|
+
unregister(ref, options) {
|
|
18
|
+
return this.request('DELETE', routes.customObject(ref), undefined, options);
|
|
19
|
+
}
|
|
20
|
+
ack(ref, deliveryIds, options) {
|
|
21
|
+
return this.request('POST', routes.customObjectAck(ref), { delivery_ids: deliveryIds }, options);
|
|
22
|
+
}
|
|
23
|
+
receive(ref, options = {}) {
|
|
24
|
+
const { after, ...requestOptions } = options;
|
|
25
|
+
if (after !== undefined && (!Number.isSafeInteger(after) || after < 0)) {
|
|
26
|
+
throw new RangeError('after must be a non-negative safe integer');
|
|
27
|
+
}
|
|
28
|
+
const path = withQuery(routes.customObjectDeliveries(ref), { after });
|
|
29
|
+
const iterate = async function* (open) {
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const abort = () => controller.abort(options.signal?.reason);
|
|
32
|
+
if (options.signal?.aborted)
|
|
33
|
+
abort();
|
|
34
|
+
else
|
|
35
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
36
|
+
let reader;
|
|
37
|
+
try {
|
|
38
|
+
const response = await open(path, { ...requestOptions, signal: controller.signal });
|
|
39
|
+
if (response.body === null)
|
|
40
|
+
return;
|
|
41
|
+
reader = response.body.getReader();
|
|
42
|
+
const decoder = new TextDecoder();
|
|
43
|
+
let buffer = '';
|
|
44
|
+
for (;;) {
|
|
45
|
+
const { done, value } = await reader.read();
|
|
46
|
+
buffer += decoder.decode(value, { stream: !done });
|
|
47
|
+
const lines = buffer.split(/\r?\n/);
|
|
48
|
+
buffer = lines.pop() ?? '';
|
|
49
|
+
if (done && buffer !== '')
|
|
50
|
+
lines.push(buffer);
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
if (line.trim() === '')
|
|
53
|
+
continue; // heartbeat
|
|
54
|
+
const delivery = JSON.parse(line);
|
|
55
|
+
yield delivery;
|
|
56
|
+
}
|
|
57
|
+
if (done)
|
|
58
|
+
return; // server deliberately closes on rotate/unregister/receiver change
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (controller.signal.aborted && options.signal?.aborted) {
|
|
63
|
+
throw new APIUserAbortError(0, 'request_aborted', 'request aborted by caller signal');
|
|
64
|
+
}
|
|
65
|
+
if (error instanceof Error && !(error instanceof SyntaxError) && !(error instanceof APIError)) {
|
|
66
|
+
throw new APIConnectionError(0, 'connection_error', error.message);
|
|
67
|
+
}
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
options.signal?.removeEventListener('abort', abort);
|
|
72
|
+
controller.abort();
|
|
73
|
+
if (reader !== undefined) {
|
|
74
|
+
try {
|
|
75
|
+
await reader.cancel();
|
|
76
|
+
}
|
|
77
|
+
catch { /* the server may have already closed */ }
|
|
78
|
+
reader.releaseLock();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
return iterate(this.open);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type FileEncoding, type FileListDTO, type FilePeekDTO, type FileWriteDTO } from '@north-light/api';
|
|
2
|
+
import type { RequestOptions } from '../types.js';
|
|
3
|
+
import type { Request } from './request.js';
|
|
4
|
+
/** Options for `files.download`. */
|
|
5
|
+
export interface FileDownloadOptions {
|
|
6
|
+
/** A single `Range` header value, e.g. `bytes=0-0` or `bytes=-500`; the answer is then 206 with `Content-Range`. */
|
|
7
|
+
range?: string;
|
|
8
|
+
/** Aborts the request and the body stream. */
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
}
|
|
11
|
+
/** Opens a raw download; the client supplies the bearer token and refresh. */
|
|
12
|
+
export type FileDownloadOpener = (path: string, options: FileDownloadOptions) => Promise<Response>;
|
|
13
|
+
export declare class Files {
|
|
14
|
+
private readonly request;
|
|
15
|
+
private readonly openDownload?;
|
|
16
|
+
constructor(request: Request, openDownload?: FileDownloadOpener | undefined);
|
|
17
|
+
read(path: string, options?: {
|
|
18
|
+
encoding?: FileEncoding;
|
|
19
|
+
} & RequestOptions): Promise<FilePeekDTO>;
|
|
20
|
+
write(path: string, content: string, options?: {
|
|
21
|
+
encoding?: FileEncoding;
|
|
22
|
+
} & RequestOptions): Promise<FileWriteDTO>;
|
|
23
|
+
list(path: string, options?: {
|
|
24
|
+
limit?: number;
|
|
25
|
+
} & RequestOptions): Promise<FileListDTO>;
|
|
26
|
+
/**
|
|
27
|
+
* The raw bytes of a file the caller may read (app listener only). Answers the `Response`
|
|
28
|
+
* itself (200, or 206 for `range`) with `Content-Type`, `Content-Length`, and `Content-Range`,
|
|
29
|
+
* so its body can be piped on. A non-2xx answer is thrown as `APIError` — a bad range is
|
|
30
|
+
* 416 `invalid_request` with `param: 'range'` — and never returned as bytes. A body cut
|
|
31
|
+
* short mid-stream means the file could not be read to the end.
|
|
32
|
+
*/
|
|
33
|
+
download(path: string, options?: FileDownloadOptions): Promise<Response>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { routes, } from '@north-light/api';
|
|
2
|
+
import { withQuery } from './request.js';
|
|
3
|
+
export class Files {
|
|
4
|
+
request;
|
|
5
|
+
openDownload;
|
|
6
|
+
constructor(request, openDownload) {
|
|
7
|
+
this.request = request;
|
|
8
|
+
this.openDownload = openDownload;
|
|
9
|
+
}
|
|
10
|
+
read(path, options) {
|
|
11
|
+
const { encoding, ...requestOptions } = options ?? {};
|
|
12
|
+
return this.request('GET', withQuery(routes.filePeek(), { path, encoding }), undefined, requestOptions);
|
|
13
|
+
}
|
|
14
|
+
write(path, content, options) {
|
|
15
|
+
const { encoding, ...requestOptions } = options ?? {};
|
|
16
|
+
return this.request('POST', routes.fileWrite(), { path, content, encoding }, requestOptions);
|
|
17
|
+
}
|
|
18
|
+
list(path, options) {
|
|
19
|
+
const { limit, ...requestOptions } = options ?? {};
|
|
20
|
+
return this.request('GET', withQuery(routes.fileList(), { path, limit }), undefined, requestOptions);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The raw bytes of a file the caller may read (app listener only). Answers the `Response`
|
|
24
|
+
* itself (200, or 206 for `range`) with `Content-Type`, `Content-Length`, and `Content-Range`,
|
|
25
|
+
* so its body can be piped on. A non-2xx answer is thrown as `APIError` — a bad range is
|
|
26
|
+
* 416 `invalid_request` with `param: 'range'` — and never returned as bytes. A body cut
|
|
27
|
+
* short mid-stream means the file could not be read to the end.
|
|
28
|
+
*/
|
|
29
|
+
async download(path, options = {}) {
|
|
30
|
+
if (!this.openDownload)
|
|
31
|
+
throw new TypeError('files.download requires a Crouter client');
|
|
32
|
+
return this.openDownload(path, options);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What one turn asks of the follower when it ends:
|
|
3
|
+
* - `requeue`: the item goes to the back of the queue at once (while the app still returns it).
|
|
4
|
+
* - `park`: the item rests until a sweep queues it again.
|
|
5
|
+
* - `drop`: the item is left alone until the next sweep (connection trouble; the turn already reported it).
|
|
6
|
+
* - `fail`: the item backs off (5 s doubling to 5 min; the turn already reported the error), then a sweep queues it.
|
|
7
|
+
*/
|
|
8
|
+
export type TurnResult = 'requeue' | 'park' | 'drop' | 'fail';
|
|
9
|
+
export interface FollowerHooks<T> {
|
|
10
|
+
/** The followed set, read from the app's rows at `start()`, every `sweepMs`, on `nudge()` and on `sweep()`. */
|
|
11
|
+
items: () => Promise<T[]> | T[];
|
|
12
|
+
key: (item: T) => string;
|
|
13
|
+
/** One turn for one item. `signal` aborts on `stop()`. A throw is treated as `fail` with that error. */
|
|
14
|
+
turn: (item: T, signal: AbortSignal) => Promise<TurnResult>;
|
|
15
|
+
/** Whether a sweep may queue this item (default: always). */
|
|
16
|
+
admit?: (key: string) => boolean;
|
|
17
|
+
/** The app stopped returning this key: clear what the watcher keeps for it. */
|
|
18
|
+
onForget?: (key: string) => void;
|
|
19
|
+
onError?: (error: unknown, item: T | null) => void;
|
|
20
|
+
sweepMs: number;
|
|
21
|
+
concurrency: number;
|
|
22
|
+
/** For the "cannot be started again" message. */
|
|
23
|
+
name: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The machinery `RunWatcher` and `GrantWatcher` share: the sweep loop (`sweepMs`, `nudge`, sweep-again coalescing),
|
|
27
|
+
* the queue and `concurrency` slots, per-key backoff, the lifetime `AbortController` and `stop()`, and `onError`.
|
|
28
|
+
* What a turn does, and whether it re-queues, is the watcher's.
|
|
29
|
+
*/
|
|
30
|
+
export declare class Follower<T> {
|
|
31
|
+
private readonly hooks;
|
|
32
|
+
private readonly following;
|
|
33
|
+
/** Keys queued at least once and not forgotten since. */
|
|
34
|
+
private readonly known;
|
|
35
|
+
/** Keys the last sweep read from `items()`. */
|
|
36
|
+
private wanted;
|
|
37
|
+
/** Keys backing off after a failure, until this instant. */
|
|
38
|
+
private readonly retryAt;
|
|
39
|
+
private readonly backoff;
|
|
40
|
+
private readonly queue;
|
|
41
|
+
private active;
|
|
42
|
+
private running;
|
|
43
|
+
private timer;
|
|
44
|
+
private sweeping;
|
|
45
|
+
private sweepAgain;
|
|
46
|
+
private readonly lifetime;
|
|
47
|
+
constructor(hooks: FollowerHooks<T>);
|
|
48
|
+
get isRunning(): boolean;
|
|
49
|
+
/** Whether the last sweep still returned this key. */
|
|
50
|
+
wants(key: string): boolean;
|
|
51
|
+
start(): Promise<void>;
|
|
52
|
+
nudge(): void;
|
|
53
|
+
stop(): Promise<void>;
|
|
54
|
+
/** A delivery landed for this key: clear its backoff. */
|
|
55
|
+
succeeded(key: string): void;
|
|
56
|
+
sweep(): Promise<void>;
|
|
57
|
+
private sweepOnce;
|
|
58
|
+
private forget;
|
|
59
|
+
private drain;
|
|
60
|
+
private run;
|
|
61
|
+
/** Back off this key: 5 s doubling to 5 min, then a sweep picks it up again. */
|
|
62
|
+
private fail;
|
|
63
|
+
report(error: unknown, item: T | null): void;
|
|
64
|
+
}
|
|
65
|
+
/** No stored connection, or a refusal `OAuth2Client` already handled: the item is dropped until the next sweep. */
|
|
66
|
+
export declare function isConnectionTrouble(error: unknown): boolean;
|
|
67
|
+
/** A turn's failure to open the person's client or read for them: reported to `onError`, then `drop` for connection
|
|
68
|
+
* trouble (the next sweep decides) or `fail` (back off) for anything else. */
|
|
69
|
+
export declare function troubleResult<T>(follower: Follower<T>, error: unknown, item: T): TurnResult;
|
|
70
|
+
/** Abortable pause; resolves early (without throwing) when `signal` aborts. */
|
|
71
|
+
export declare function pause(ms: number, signal: AbortSignal): Promise<void>;
|