@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.
Files changed (111) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +187 -2
  3. package/dist/client.d.ts +162 -0
  4. package/dist/client.js +543 -0
  5. package/dist/error-codes.d.ts +7 -0
  6. package/dist/error-codes.js +41 -0
  7. package/dist/errors.d.ts +57 -0
  8. package/dist/errors.js +87 -0
  9. package/dist/index.d.ts +41 -0
  10. package/dist/index.js +17 -0
  11. package/dist/keygen-cli.d.ts +2 -0
  12. package/dist/keygen-cli.js +6 -0
  13. package/dist/keygen-command.d.ts +6 -0
  14. package/dist/keygen-command.js +157 -0
  15. package/dist/oauth/index.d.ts +217 -0
  16. package/dist/oauth/index.js +535 -0
  17. package/dist/oauth/keygen.d.ts +18 -0
  18. package/dist/oauth/keygen.js +46 -0
  19. package/dist/oauth/refusal.d.ts +32 -0
  20. package/dist/oauth/refusal.js +17 -0
  21. package/dist/resources/activity.d.ts +22 -0
  22. package/dist/resources/activity.js +43 -0
  23. package/dist/resources/attachments.d.ts +15 -0
  24. package/dist/resources/attachments.js +13 -0
  25. package/dist/resources/bash.d.ts +9 -0
  26. package/dist/resources/bash.js +15 -0
  27. package/dist/resources/canvas/history.d.ts +11 -0
  28. package/dist/resources/canvas/history.js +20 -0
  29. package/dist/resources/canvas.d.ts +19 -0
  30. package/dist/resources/canvas.js +42 -0
  31. package/dist/resources/crons.d.ts +15 -0
  32. package/dist/resources/crons.js +33 -0
  33. package/dist/resources/custom-objects.d.ts +20 -0
  34. package/dist/resources/custom-objects.js +84 -0
  35. package/dist/resources/files.d.ts +34 -0
  36. package/dist/resources/files.js +34 -0
  37. package/dist/resources/follower.d.ts +71 -0
  38. package/dist/resources/follower.js +202 -0
  39. package/dist/resources/forward.d.ts +7 -0
  40. package/dist/resources/forward.js +67 -0
  41. package/dist/resources/grant-watcher.d.ts +57 -0
  42. package/dist/resources/grant-watcher.js +100 -0
  43. package/dist/resources/human/inbox.d.ts +14 -0
  44. package/dist/resources/human/inbox.js +30 -0
  45. package/dist/resources/human/requests.d.ts +13 -0
  46. package/dist/resources/human/requests.js +26 -0
  47. package/dist/resources/human.d.ts +8 -0
  48. package/dist/resources/human.js +10 -0
  49. package/dist/resources/identifiers.d.ts +8 -0
  50. package/dist/resources/identifiers.js +33 -0
  51. package/dist/resources/memory.d.ts +69 -0
  52. package/dist/resources/memory.js +30 -0
  53. package/dist/resources/models/config.d.ts +8 -0
  54. package/dist/resources/models/config.js +9 -0
  55. package/dist/resources/models/credentials.d.ts +10 -0
  56. package/dist/resources/models/credentials.js +17 -0
  57. package/dist/resources/models.d.ts +8 -0
  58. package/dist/resources/models.js +10 -0
  59. package/dist/resources/node-stream.d.ts +34 -0
  60. package/dist/resources/node-stream.js +176 -0
  61. package/dist/resources/nodes/jobs.d.ts +9 -0
  62. package/dist/resources/nodes/jobs.js +14 -0
  63. package/dist/resources/nodes/result.d.ts +8 -0
  64. package/dist/resources/nodes/result.js +11 -0
  65. package/dist/resources/nodes/worktree.d.ts +9 -0
  66. package/dist/resources/nodes/worktree.js +14 -0
  67. package/dist/resources/nodes.d.ts +23 -0
  68. package/dist/resources/nodes.js +47 -0
  69. package/dist/resources/providers.d.ts +51 -0
  70. package/dist/resources/providers.js +15 -0
  71. package/dist/resources/questions.d.ts +49 -0
  72. package/dist/resources/questions.js +21 -0
  73. package/dist/resources/request.d.ts +7 -0
  74. package/dist/resources/request.js +19 -0
  75. package/dist/resources/run-notice.d.ts +34 -0
  76. package/dist/resources/run-notice.js +67 -0
  77. package/dist/resources/run-reply.d.ts +79 -0
  78. package/dist/resources/run-reply.js +93 -0
  79. package/dist/resources/run-stream.d.ts +65 -0
  80. package/dist/resources/run-stream.js +243 -0
  81. package/dist/resources/run-watcher.d.ts +98 -0
  82. package/dist/resources/run-watcher.js +162 -0
  83. package/dist/resources/runs.d.ts +192 -0
  84. package/dist/resources/runs.js +208 -0
  85. package/dist/resources/shares.d.ts +40 -0
  86. package/dist/resources/shares.js +19 -0
  87. package/dist/resources/structured-requests.d.ts +14 -0
  88. package/dist/resources/structured-requests.js +19 -0
  89. package/dist/resources/uploads.d.ts +27 -0
  90. package/dist/resources/uploads.js +11 -0
  91. package/dist/schema.d.ts +7 -0
  92. package/dist/schema.js +10 -0
  93. package/dist/stores/postgres.d.ts +32 -0
  94. package/dist/stores/postgres.js +92 -0
  95. package/dist/testing/cloud.d.ts +209 -0
  96. package/dist/testing/cloud.js +880 -0
  97. package/dist/testing/index.d.ts +5 -0
  98. package/dist/testing/index.js +3 -0
  99. package/dist/testing/notices.d.ts +70 -0
  100. package/dist/testing/notices.js +205 -0
  101. package/dist/testing/run.d.ts +172 -0
  102. package/dist/testing/run.js +429 -0
  103. package/dist/testing/users.d.ts +121 -0
  104. package/dist/testing/users.js +70 -0
  105. package/dist/testing/wire.d.ts +62 -0
  106. package/dist/testing/wire.js +148 -0
  107. package/dist/types.d.ts +80 -0
  108. package/dist/types.js +1 -0
  109. package/dist/version.d.ts +2 -0
  110. package/dist/version.js +3 -0
  111. package/package.json +56 -5
package/dist/errors.js ADDED
@@ -0,0 +1,87 @@
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 caller's `AbortSignal` fired. */
50
+ export class APIUserAbortError extends APIError {
51
+ }
52
+ /** Maps a transport failure or a plain `APIError` onto the specific class for its status and code. */
53
+ export function mapError(error) {
54
+ if (!(error instanceof APIError) || error.constructor !== APIError) {
55
+ return error instanceof APIError ? error : new APIConnectionError(0, 'connection_error', error instanceof Error ? error.message : String(error));
56
+ }
57
+ const Constructor = error.code === 'runtime_version_unsupported'
58
+ ? RuntimeVersionError
59
+ : error.code === 'request_aborted'
60
+ ? APIUserAbortError
61
+ : error.status === 504 || error.code === 'request_timeout'
62
+ ? APIConnectionTimeoutError
63
+ : error.code === 'daemon_unavailable' || error.code === 'transport_error' || error.code === 'daemon_request_interrupted' || error.code === 'daemon_health_unavailable'
64
+ ? APIConnectionError
65
+ : error.status === 400
66
+ ? BadRequestError
67
+ : error.status === 401
68
+ ? AuthenticationError
69
+ : error.status === 403
70
+ ? PermissionDeniedError
71
+ : error.status === 404
72
+ ? NotFoundError
73
+ : error.status === 409
74
+ ? ConflictError
75
+ : error.status === 413 || error.status === 422
76
+ ? UnprocessableEntityError
77
+ : error.status === 402 || error.status === 429
78
+ ? RateLimitError
79
+ : error.status >= 500
80
+ ? InternalServerError
81
+ : APIError;
82
+ return new Constructor(error.status, error.code, error.message, error.details, error.headers, {
83
+ type: error.type, param: error.param, origin: error.origin, retryable: error.retryable,
84
+ retry_after_s: error.retryAfterS, reset_at: error.resetAt, request_id: error.requestId, run_id: error.runId,
85
+ question_id: error.questionId, scope: error.scope, user_action: error.userAction,
86
+ });
87
+ }
@@ -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, 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, InternalServerError, NotFoundError, PermissionDeniedError, RateLimitError, RuntimeVersionError, UnprocessableEntityError, mapError, } from './errors.js';
36
+ export type { 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, 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, 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,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { runKeygen } from './keygen-command.js';
3
+ runKeygen(process.argv.slice(2)).then((code) => { process.exitCode = code; }, (error) => {
4
+ console.error(error instanceof Error ? error.message : String(error));
5
+ process.exitCode = 1;
6
+ });
@@ -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,157 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { mkdir, open, unlink } from 'node:fs/promises';
3
+ import { dirname } from 'node:path';
4
+ import { createInterface } from 'node:readline';
5
+ import { generateKeyPair } from './oauth/keygen.js';
6
+ const USAGE = 'Usage: crouter-sdk keygen [--out <path>] [--app <client id>] [--yes]';
7
+ const CRTR_TIMEOUT_MS = 60_000;
8
+ function parseArgs(args) {
9
+ if (args[0] !== 'keygen')
10
+ throw new Error(USAGE);
11
+ const parsed = { path: 'local/app-key.json', yes: false };
12
+ const seen = new Set();
13
+ for (let i = 1; i < args.length; i++) {
14
+ const flag = args[i];
15
+ if (seen.has(flag))
16
+ throw new Error(USAGE);
17
+ seen.add(flag);
18
+ if (flag === '--yes') {
19
+ parsed.yes = true;
20
+ continue;
21
+ }
22
+ const value = args[++i];
23
+ if ((flag !== '--out' && flag !== '--app') || !value || value.startsWith('--'))
24
+ throw new Error(USAGE);
25
+ if (flag === '--out')
26
+ parsed.path = value;
27
+ else
28
+ parsed.app = value;
29
+ }
30
+ return parsed;
31
+ }
32
+ const manualSteps = (path) => [
33
+ 'Next, in this order:',
34
+ " 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.",
35
+ ` 2. Then point your app's privateKey at ${path} and deploy.`,
36
+ ' 3. Once a real sign-in works, retire the old key on the Sign-in tab.',
37
+ ];
38
+ const addedSteps = (path, app) => [
39
+ 'Next, in this order:',
40
+ ` 1. Point your app's privateKey at ${path} and deploy.`,
41
+ ' 2. Check that a real sign-in works.',
42
+ ` 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).`,
43
+ ];
44
+ const seconds = (ms) => {
45
+ const n = Math.max(1, Math.round(ms / 1000));
46
+ return `${n} second${n === 1 ? '' : 's'}`;
47
+ };
48
+ const say = (lines) => console.error(lines.join('\n'));
49
+ /** Asks on stderr; resolves true only for Enter or y/yes. End of input declines. */
50
+ function confirm(question) {
51
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
52
+ return new Promise((resolve) => {
53
+ let answered = false;
54
+ rl.on('close', () => { if (!answered) {
55
+ process.stderr.write('\n');
56
+ resolve(false);
57
+ } });
58
+ rl.question(`${question} `, (answer) => {
59
+ answered = true;
60
+ rl.close();
61
+ resolve(/^(y|yes)?$/i.test(answer.trim()));
62
+ });
63
+ });
64
+ }
65
+ const parseJson = (text) => {
66
+ try {
67
+ return JSON.parse(text.trim());
68
+ }
69
+ catch {
70
+ return undefined;
71
+ }
72
+ };
73
+ const lastLine = (text) => text.split('\n').map((line) => line.trim()).filter(Boolean).at(-1);
74
+ /** The `message` of a JSON error that ends `text`, even when other lines (a warning, a notice) precede it. */
75
+ const errorMessage = (text) => {
76
+ const body = parseJson(text.slice(text.lastIndexOf('\n{') + 1));
77
+ return typeof body?.message === 'string' && body.message ? body.message : undefined;
78
+ };
79
+ /** Runs `crtr cloud apps keys add` once: execFile, no shell, the caller's environment. */
80
+ function addKey(app, publicJwk, timeoutMs) {
81
+ const argv = ['cloud', 'apps', 'keys', 'add', '--app-id', app, '--jwk', publicJwk, '--json'];
82
+ return new Promise((resolve) => {
83
+ execFile('crtr', argv, { timeout: timeoutMs, env: process.env, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => {
84
+ const err = error;
85
+ if (err?.code === 'ENOENT')
86
+ return resolve({ kind: 'missing' });
87
+ if (err) {
88
+ const message = err.killed
89
+ ? `crtr did not answer within ${seconds(timeoutMs)}.`
90
+ : errorMessage(stdout) ?? errorMessage(stderr) ?? lastLine(stderr) ?? lastLine(stdout) ?? err.message;
91
+ return resolve({ kind: 'failed', message });
92
+ }
93
+ const answer = parseJson(stdout);
94
+ if (typeof answer?.kid !== 'string') {
95
+ return resolve({ kind: 'failed', message: `crtr answered without the added key: ${lastLine(stdout) ?? '(no output)'}` });
96
+ }
97
+ resolve({ kind: 'added', kid: answer.kid, created: answer.created !== false });
98
+ });
99
+ });
100
+ }
101
+ /** The `crouter-sdk keygen` command. Returns the process exit code. */
102
+ export async function runKeygen(argv, options = {}) {
103
+ const { path, app, yes } = parseArgs(argv);
104
+ await mkdir(dirname(path), { recursive: true });
105
+ // Reserve the destination before generating or printing the public key; never truncate an existing key.
106
+ const file = await open(path, 'wx', 0o600).catch((error) => {
107
+ if (error.code === 'EEXIST')
108
+ throw new Error(`${path} already exists. Delete it or pass --out to write somewhere else.`);
109
+ throw error;
110
+ });
111
+ let publicJwk;
112
+ let kid;
113
+ try {
114
+ const pair = await generateKeyPair();
115
+ await file.writeFile(`${JSON.stringify(pair.privateJwk)}\n`);
116
+ ({ publicJwk, kid } = pair);
117
+ }
118
+ catch (error) {
119
+ await file.close();
120
+ await unlink(path);
121
+ throw error;
122
+ }
123
+ await file.close();
124
+ const publicLine = JSON.stringify(publicJwk);
125
+ console.error(`Saved the private key to ${path} (key id ${kid}). Keep it out of git.`);
126
+ console.log(publicLine);
127
+ if (!app) {
128
+ say([...manualSteps(path), 'Tip: pass --app <client id> to add the key with crtr.']);
129
+ return 0;
130
+ }
131
+ if (!yes) {
132
+ if (!process.stdin.isTTY || !process.stderr.isTTY) {
133
+ say(['Not adding the key: no terminal to confirm. Pass --yes to add it with crtr.', ...manualSteps(path)]);
134
+ return 0;
135
+ }
136
+ if (!(await confirm(`Add this key to ${app} as an additional key? Your current key keeps working. [Y/n]`))) {
137
+ say(manualSteps(path));
138
+ return 0;
139
+ }
140
+ }
141
+ const outcome = await addKey(app, publicLine, options.crtrTimeoutMs ?? CRTR_TIMEOUT_MS);
142
+ if (outcome.kind === 'missing') {
143
+ say(["Couldn't find crtr, so the key was not added.", ...manualSteps(path)]);
144
+ return 0;
145
+ }
146
+ if (outcome.kind === 'failed') {
147
+ say([`Couldn't add the key: ${outcome.message}`, ...manualSteps(path)]);
148
+ return 2;
149
+ }
150
+ say([
151
+ outcome.created
152
+ ? `Added key ${outcome.kid} to ${app} as an additional key. Your current key still works.`
153
+ : `Key ${outcome.kid} is already on ${app}.`,
154
+ ...addedSteps(path, app),
155
+ ]);
156
+ return 0;
157
+ }
@@ -0,0 +1,217 @@
1
+ import { type JWK } from 'jose';
2
+ import { type IdTokenClaims } from '@north-light/identity';
3
+ import { Crouter, type CrouterOptions } from '../client.js';
4
+ import { RunWatcher, type RunWatchOptions } from '../resources/run-watcher.js';
5
+ import { GrantWatcher, type GrantWatchOptions } from '../resources/grant-watcher.js';
6
+ import { type Refusal } from './refusal.js';
7
+ export { refusalOf } from './refusal.js';
8
+ export type { Refusal, RefusalKind } from './refusal.js';
9
+ /** One connection `refreshIdleConnections` could not refresh and that is still stored: an error other than a
10
+ * refusal, or a refusal whose `onRefusal` threw (then `refusalOf(error)` is set and `error.cause` is what it threw). */
11
+ export interface RefreshFailure {
12
+ userId: string;
13
+ error: unknown;
14
+ }
15
+ export interface Connection {
16
+ userId: string;
17
+ runtimeUrl: string;
18
+ refreshToken: string;
19
+ grantId: string;
20
+ accessToken: string;
21
+ accessTokenExpiresAt: number;
22
+ idToken?: string;
23
+ }
24
+ export interface ConnectionStore {
25
+ load(userId: string): Promise<Connection | null>;
26
+ save(connection: Connection): Promise<void>;
27
+ /** Run `fn` holding `userId`'s lock across every process that shares the store, and commit what it wrote when
28
+ * it resolves. `OAuth2Client` reloads, refreshes and saves inside it, so two processes never spend one refresh token. */
29
+ lock<T>(userId: string, fn: () => Promise<T>): Promise<T>;
30
+ /** Delete `userId`'s connection only while its refresh token is still `refreshToken`, and answer whether a row
31
+ * was deleted. The SDK calls it, under `lock`, after a refused refresh's `onRefusal` resolves; a connection a
32
+ * fresh sign-in saved meanwhile carries another token and is kept. */
33
+ remove(userId: string, refreshToken: string): Promise<boolean>;
34
+ /** Connections last saved before this instant. A save must update the store's activity timestamp. */
35
+ listStale?(before: Date): Promise<Connection[]>;
36
+ }
37
+ /** A connection store for one process (local development and tests): its lock does not reach other processes. */
38
+ export declare class MemoryConnectionStore implements ConnectionStore {
39
+ private readonly connections;
40
+ private readonly locks;
41
+ private readonly savedAt;
42
+ load(userId: string): Promise<Connection | null>;
43
+ save(connection: Connection): Promise<void>;
44
+ remove(userId: string, refreshToken: string): Promise<boolean>;
45
+ listStale(before: Date): Promise<Connection[]>;
46
+ /** Not reentrant: calling `lock` for the same user from inside `fn` waits forever. */
47
+ lock<T>(userId: string, fn: () => Promise<T>): Promise<T>;
48
+ }
49
+ /**
50
+ * A person's crouter cloud identity from a sign-in-only (`openid`, `openid email`, `openid profile`, or `openid email profile`) request, read from an
51
+ * ID token the SDK verified: ES256 signature against the directory's JWKS, `iss`, `aud` = the client id,
52
+ * `exp`, and the `nonce` from `authorizeUrl`. It is not a connection: it holds no runtime URL, refresh
53
+ * token, or grant, and nothing can be called on the person's runtime with it.
54
+ */
55
+ export interface Identity {
56
+ /** The person's crouter cloud user id (`user:<id>`), the same `sub` a `Connection.userId` carries. */
57
+ sub: string;
58
+ /** Present only when `email` was requested and approved. */
59
+ email?: string;
60
+ emailVerified?: boolean;
61
+ /** Present only when `profile` was requested and approved and the person has a name (a phone-only account has none). The full name, e.g. `Sam Rivera`. */
62
+ name?: string;
63
+ givenName?: string;
64
+ familyName?: string;
65
+ /** Present only when `profile` was requested and approved and the ID token carries an https `picture` (the person's Google photo URL). An account with no photo has none; show initials then. */
66
+ picture?: string;
67
+ /** Every verified ID token claim. */
68
+ claims: IdTokenClaims;
69
+ }
70
+ /** `authorizeUrl`'s nonce: `string` when the scopes are known to include `openid`, `undefined` when known not to, else either. */
71
+ export type AuthorizeNonce<S extends readonly string[]> = string extends S[number] ? string | undefined : 'openid' extends S[number] ? string : undefined;
72
+ /** The `profile` claims a verified ID token carries, each only when present: use it on `Connection.idToken` after `verifyIdToken` for a sign-in that requested runtime scopes (`exchangeCode`), as `exchangeIdentity` does for a sign-in-only one. */
73
+ export declare function profileFromClaims(claims: IdTokenClaims): Pick<Identity, 'name' | 'givenName' | 'familyName' | 'picture'>;
74
+ export interface OAuth2Options {
75
+ clientId: string;
76
+ privateKey: string | JWK;
77
+ keyId?: string;
78
+ redirectUri: string;
79
+ issuer: string;
80
+ /** Where the app keeps each person's `Connection`. Save `exchangeCode`'s result here; every refresh reloads,
81
+ * rotates and saves under its `lock`, and a refused connection is removed from it. */
82
+ store: ConnectionStore;
83
+ /**
84
+ * Called when the directory refuses to refresh a stored connection (`grant_removed`, `grant_suspended`,
85
+ * `refresh_token_expired`, `refresh_token_reused`, `grant_revoked`): act on the person's data here, once, for
86
+ * every caller (a request, `refreshIdleConnections`, a `RunWatcher`). `refusal.kind` says what it means.
87
+ *
88
+ * At least once per dead connection, so make it idempotent: it runs after the store lock is released, and the
89
+ * SDK removes the stored connection (only while it still holds the refused token) after it resolves. If it
90
+ * throws, the connection stays stored and the next use of it refreshes, is refused, and calls it again.
91
+ * It must not call the runtime for `refusal.userId` (that connection is dead): `oauth.client(userId)` for that
92
+ * user inside it throws `CrouterError` `refusal_handler_reentry`.
93
+ */
94
+ onRefusal?: (refusal: Refusal) => void | Promise<void>;
95
+ /** Transport override for the directory requests and for the runtime calls of every `client(userId)`. */
96
+ fetch?: typeof fetch;
97
+ }
98
+ /** A runtime client on one person's stored connection (`oauth.client(userId)`): a `Crouter` that refreshes its
99
+ * token through the `OAuth2Client`'s store, plus whose connection it is and the runtime it calls. */
100
+ export declare class ConnectedCrouter extends Crouter {
101
+ /** The person whose connection this client uses (`user:<id>`). */
102
+ readonly userId: string;
103
+ /** The person's runtime URL this client calls, as the stored connection named it. */
104
+ readonly runtimeUrl: string;
105
+ constructor(connection: Pick<Connection, 'userId' | 'runtimeUrl'>, options: Pick<CrouterOptions, 'tokenSource' | 'onUnauthorized' | 'fetch'>);
106
+ }
107
+ export declare class OAuth2Client {
108
+ private readonly options;
109
+ private readonly issuer;
110
+ private readonly fetcher;
111
+ private readonly store;
112
+ private jwks?;
113
+ private discoveryPromise?;
114
+ /** One refresh per (user, refresh token) in this process; a client holding a newer token never joins an older refusal. */
115
+ private readonly inFlight;
116
+ /** Refusals whose connection the SDK removed (or found already replaced) after `onRefusal` resolved. */
117
+ private readonly settled;
118
+ constructor(options: OAuth2Options);
119
+ /** The issuer's discovery document. The first successful read is kept for the client's life; a read that fails or
120
+ * takes longer than 10 s rejects and is forgotten, so the next call reads again. */
121
+ private discovery;
122
+ /**
123
+ * The app's page on the signed-in person's crouter cloud account, where they change or remove what the app may do:
124
+ * the discovery document's `account_app_uri_template` with its one `{client_id}` replaced by the encoded client id.
125
+ * Needs no user or connection; its only request is the shared discovery read. Throws `CrouterError` when the field
126
+ * is absent, does not contain `{client_id}` exactly once, or does not expand to a URL on the issuer's origin.
127
+ */
128
+ accountAppUrl(): Promise<string>;
129
+ /** Start a sign-in. When `scopes` includes `openid` the result carries the `nonce` to pass to the exchange; with literal scopes its type is `string`.
130
+ * `prompt: 'consent'` makes the directory show its consent screen even when the person already approved these scopes. */
131
+ authorizeUrl<const S extends readonly string[]>({ scopes, state, codeVerifier, prompt }: {
132
+ scopes: S;
133
+ state: string;
134
+ codeVerifier?: string;
135
+ prompt?: 'consent';
136
+ }): Promise<{
137
+ url: string;
138
+ codeVerifier: string;
139
+ state: string;
140
+ nonce: AuthorizeNonce<S>;
141
+ }>;
142
+ /**
143
+ * Finish a sign-in that asked for runtime scopes and return the person's `Connection`. A sign-in-only
144
+ * request (`openid`, `openid email`, `openid profile`, or `openid email profile`) gets no connection from the directory; finish it with
145
+ * `exchangeIdentity`, which this method names when it receives such a response.
146
+ */
147
+ exchangeCode({ code, codeVerifier, state, expectedState, nonce }: {
148
+ code: string;
149
+ codeVerifier: string;
150
+ state: string;
151
+ expectedState: string;
152
+ nonce?: string;
153
+ }): Promise<Connection>;
154
+ /**
155
+ * Finish a sign-in-only request (`authorizeUrl` with scopes `openid` or `openid email`) and return the
156
+ * person's verified identity. There is no connection, refresh token, or runtime access on this path.
157
+ * `nonce` is the value `authorizeUrl` returned and is required. A response without an ID token, or with
158
+ * one that fails verification, throws `id_token_invalid`; a response carrying a runtime connection (the
159
+ * request asked for runtime scopes) throws `oauth_exchange_mismatch` and must be finished with `exchangeCode`.
160
+ */
161
+ exchangeIdentity({ code, codeVerifier, state, expectedState, nonce }: {
162
+ code: string;
163
+ codeVerifier: string;
164
+ state: string;
165
+ expectedState: string;
166
+ nonce: string;
167
+ }): Promise<Identity>;
168
+ /**
169
+ * Refresh the store's idle connections once (the app schedules it, for example daily, with `olderThanMs` of 30
170
+ * days). Each refusal goes through `onRefusal` and the connection's removal, and is counted in `refused`.
171
+ * `failures` holds every connection still stored that could not be refreshed: an error other than a refusal,
172
+ * or a refusal whose `onRefusal` threw (`refusalOf(failure.error)` is set and `failure.error.cause` is what the
173
+ * handler threw); the next run retries those. A connection removed before its turn is skipped.
174
+ */
175
+ refreshIdleConnections({ olderThanMs }: {
176
+ olderThanMs: number;
177
+ }): Promise<{
178
+ refreshed: number;
179
+ refused: number;
180
+ failures: RefreshFailure[];
181
+ }>;
182
+ /**
183
+ * Rotate `connection`'s refresh token through the store: under the user's lock it reloads, and when another
184
+ * process already rotated (or the person signed in again) it returns the stored connection without calling the
185
+ * directory. Nothing stored throws `CrouterError` `not_connected`. A refusal runs `onRefusal`, removes the
186
+ * connection, and rejects with the directory's `AuthenticationError` (`refusalOf(error)` set).
187
+ */
188
+ refresh(connection: Connection): Promise<Connection>;
189
+ private rotate;
190
+ /**
191
+ * A runtime client on `userId`'s stored connection; `runtimeUrl` on it names the runtime it calls. Nothing
192
+ * stored throws `CrouterError` `not_connected` (show "sign in again"). The client refreshes through the store;
193
+ * once its connection is refused (and `onRefusal` resolved) it throws that same refusal on every later call
194
+ * without asking the directory again. Called for the user whose `onRefusal` is running, it throws
195
+ * `refusal_handler_reentry`.
196
+ */
197
+ client(userId: string): Promise<ConnectedCrouter>;
198
+ /**
199
+ * Follow the app's background runs until each comes to rest, and hand each ending to `onEnded` (see
200
+ * `RunWatchOptions`). Runs are opened with `client(userId)`, so refusals reach `onRefusal`. Call `start()`.
201
+ */
202
+ watchRuns(options: RunWatchOptions): RunWatcher;
203
+ /**
204
+ * Follow the grants of the people `users()` names, and hand each change (and each user's first reading in this
205
+ * process) to `onChanged` (see `GrantWatchOptions`). Grants are read with `client(userId)`, so refusals reach
206
+ * `onRefusal`. Call `start()`.
207
+ */
208
+ watchGrants(options: GrantWatchOptions): GrantWatcher;
209
+ /** Verify an ID token: ES256 signature against the discovered JWKS, `iss`, `aud` = client id, `exp`, and `nonce` when given. A token that fails any check throws `id_token_invalid`; a JWKS that can't be fetched or is malformed rethrows that error. */
210
+ verifyIdToken(idToken: string, { nonce }?: {
211
+ nonce?: string;
212
+ }): Promise<IdTokenClaims>;
213
+ revoke(connection: Connection): Promise<boolean>;
214
+ private tokenResponse;
215
+ private connectionFrom;
216
+ private post;
217
+ }