@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.
Files changed (121) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +187 -2
  3. package/dist/client.d.ts +176 -0
  4. package/dist/client.js +582 -0
  5. package/dist/crtr.d.ts +24 -0
  6. package/dist/crtr.js +50 -0
  7. package/dist/env-command.d.ts +15 -0
  8. package/dist/env-command.js +138 -0
  9. package/dist/error-codes.d.ts +7 -0
  10. package/dist/error-codes.js +43 -0
  11. package/dist/errors.d.ts +72 -0
  12. package/dist/errors.js +101 -0
  13. package/dist/index.d.ts +41 -0
  14. package/dist/index.js +17 -0
  15. package/dist/keygen-cli.d.ts +2 -0
  16. package/dist/keygen-cli.js +11 -0
  17. package/dist/keygen-command.d.ts +6 -0
  18. package/dist/keygen-command.js +131 -0
  19. package/dist/oauth/index.d.ts +235 -0
  20. package/dist/oauth/index.js +621 -0
  21. package/dist/oauth/keygen.d.ts +18 -0
  22. package/dist/oauth/keygen.js +46 -0
  23. package/dist/oauth/refusal.d.ts +32 -0
  24. package/dist/oauth/refusal.js +17 -0
  25. package/dist/resources/activity.d.ts +22 -0
  26. package/dist/resources/activity.js +43 -0
  27. package/dist/resources/attachments.d.ts +15 -0
  28. package/dist/resources/attachments.js +13 -0
  29. package/dist/resources/bash.d.ts +9 -0
  30. package/dist/resources/bash.js +15 -0
  31. package/dist/resources/canvas/history.d.ts +11 -0
  32. package/dist/resources/canvas/history.js +20 -0
  33. package/dist/resources/canvas.d.ts +19 -0
  34. package/dist/resources/canvas.js +42 -0
  35. package/dist/resources/crons.d.ts +15 -0
  36. package/dist/resources/crons.js +33 -0
  37. package/dist/resources/custom-objects.d.ts +20 -0
  38. package/dist/resources/custom-objects.js +84 -0
  39. package/dist/resources/files.d.ts +34 -0
  40. package/dist/resources/files.js +34 -0
  41. package/dist/resources/follower.d.ts +71 -0
  42. package/dist/resources/follower.js +202 -0
  43. package/dist/resources/forward.d.ts +7 -0
  44. package/dist/resources/forward.js +67 -0
  45. package/dist/resources/grant-watcher.d.ts +57 -0
  46. package/dist/resources/grant-watcher.js +100 -0
  47. package/dist/resources/human/inbox.d.ts +14 -0
  48. package/dist/resources/human/inbox.js +30 -0
  49. package/dist/resources/human/requests.d.ts +13 -0
  50. package/dist/resources/human/requests.js +26 -0
  51. package/dist/resources/human.d.ts +8 -0
  52. package/dist/resources/human.js +10 -0
  53. package/dist/resources/identifiers.d.ts +8 -0
  54. package/dist/resources/identifiers.js +33 -0
  55. package/dist/resources/memory.d.ts +69 -0
  56. package/dist/resources/memory.js +30 -0
  57. package/dist/resources/models/config.d.ts +8 -0
  58. package/dist/resources/models/config.js +9 -0
  59. package/dist/resources/models/credentials.d.ts +10 -0
  60. package/dist/resources/models/credentials.js +17 -0
  61. package/dist/resources/models.d.ts +8 -0
  62. package/dist/resources/models.js +10 -0
  63. package/dist/resources/node-stream.d.ts +34 -0
  64. package/dist/resources/node-stream.js +176 -0
  65. package/dist/resources/nodes/jobs.d.ts +9 -0
  66. package/dist/resources/nodes/jobs.js +14 -0
  67. package/dist/resources/nodes/result.d.ts +8 -0
  68. package/dist/resources/nodes/result.js +11 -0
  69. package/dist/resources/nodes/worktree.d.ts +9 -0
  70. package/dist/resources/nodes/worktree.js +14 -0
  71. package/dist/resources/nodes.d.ts +23 -0
  72. package/dist/resources/nodes.js +47 -0
  73. package/dist/resources/providers.d.ts +51 -0
  74. package/dist/resources/providers.js +15 -0
  75. package/dist/resources/questions.d.ts +49 -0
  76. package/dist/resources/questions.js +21 -0
  77. package/dist/resources/request.d.ts +12 -0
  78. package/dist/resources/request.js +50 -0
  79. package/dist/resources/run-notice.d.ts +34 -0
  80. package/dist/resources/run-notice.js +67 -0
  81. package/dist/resources/run-reply.d.ts +79 -0
  82. package/dist/resources/run-reply.js +93 -0
  83. package/dist/resources/run-stream.d.ts +65 -0
  84. package/dist/resources/run-stream.js +243 -0
  85. package/dist/resources/run-watcher.d.ts +98 -0
  86. package/dist/resources/run-watcher.js +162 -0
  87. package/dist/resources/runs.d.ts +199 -0
  88. package/dist/resources/runs.js +228 -0
  89. package/dist/resources/shares.d.ts +40 -0
  90. package/dist/resources/shares.js +19 -0
  91. package/dist/resources/structured-requests.d.ts +14 -0
  92. package/dist/resources/structured-requests.js +19 -0
  93. package/dist/resources/uploads.d.ts +27 -0
  94. package/dist/resources/uploads.js +11 -0
  95. package/dist/schema.d.ts +7 -0
  96. package/dist/schema.js +10 -0
  97. package/dist/stores/postgres.d.ts +32 -0
  98. package/dist/stores/postgres.js +92 -0
  99. package/dist/testing/clock.d.ts +7 -0
  100. package/dist/testing/clock.js +12 -0
  101. package/dist/testing/cloud.d.ts +228 -0
  102. package/dist/testing/cloud.js +914 -0
  103. package/dist/testing/conformance/index.d.ts +2 -0
  104. package/dist/testing/conformance/index.js +3 -0
  105. package/dist/testing/conformance/keyed-write.d.ts +43 -0
  106. package/dist/testing/conformance/keyed-write.js +124 -0
  107. package/dist/testing/index.d.ts +7 -0
  108. package/dist/testing/index.js +4 -0
  109. package/dist/testing/notices.d.ts +76 -0
  110. package/dist/testing/notices.js +209 -0
  111. package/dist/testing/run.d.ts +174 -0
  112. package/dist/testing/run.js +429 -0
  113. package/dist/testing/users.d.ts +127 -0
  114. package/dist/testing/users.js +78 -0
  115. package/dist/testing/wire.d.ts +62 -0
  116. package/dist/testing/wire.js +148 -0
  117. package/dist/types.d.ts +88 -0
  118. package/dist/types.js +1 -0
  119. package/dist/version.d.ts +2 -0
  120. package/dist/version.js +3 -0
  121. package/package.json +60 -5
@@ -0,0 +1,235 @@
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
+ * One deadline, in milliseconds, for each directory operation: a discovery read, a token request (the code
99
+ * exchange or a refresh) or a revocation. It covers every attempt of that operation, the reading of the answer
100
+ * and every `runtime_provisioning` retry wait, including a `Retry-After`; a wait that would pass it fails at
101
+ * once. A refresh runs this bound while it holds the user's store lock, so the lock is held at most this long
102
+ * (plus the store's own time) and a hung directory cannot stall the user's other refreshes. Defaults to
103
+ * `DEFAULT_DIRECTORY_TIMEOUT_MS` (10 s). The error is `DeadlineExceededError` (`code: 'deadline_exceeded'`).
104
+ */
105
+ timeoutMs?: number;
106
+ }
107
+ /** A runtime client on one person's stored connection (`oauth.client(userId)`): a `Crouter` that refreshes its
108
+ * token through the `OAuth2Client`'s store, plus whose connection it is and the runtime it calls. */
109
+ export declare class ConnectedCrouter extends Crouter {
110
+ /** The person whose connection this client uses (`user:<id>`). */
111
+ readonly userId: string;
112
+ /** The person's runtime URL this client calls, as the stored connection named it. */
113
+ readonly runtimeUrl: string;
114
+ constructor(connection: Pick<Connection, 'userId' | 'runtimeUrl'>, options: Pick<CrouterOptions, 'tokenSource' | 'onUnauthorized' | 'fetch'>);
115
+ }
116
+ /** The default deadline of one directory operation (`OAuth2Options.timeoutMs`): a discovery read is forgotten when it passes. */
117
+ export declare const DEFAULT_DIRECTORY_TIMEOUT_MS = 10000;
118
+ export declare class OAuth2Client {
119
+ private readonly options;
120
+ private readonly issuer;
121
+ private readonly fetcher;
122
+ private readonly timeoutMs;
123
+ private readonly store;
124
+ private jwks?;
125
+ private discoveryPromise?;
126
+ /** One refresh per (user, refresh token) in this process; a client holding a newer token never joins an older refusal. */
127
+ private readonly inFlight;
128
+ /** Refusals whose connection the SDK removed (or found already replaced) after `onRefusal` resolved. */
129
+ private readonly settled;
130
+ constructor(options: OAuth2Options);
131
+ /** The issuer's discovery document. The first successful read is kept for the client's life; a read that fails or
132
+ * passes the deadline (`timeoutMs`) rejects and is forgotten, so the next call reads again. */
133
+ private discovery;
134
+ /** One directory request and the reading of its answer under `deadlineAt`; a deadline rejects `DeadlineExceededError`. */
135
+ private bounded;
136
+ /** The discovery document within the operation's `deadlineAt`: the memoised read keeps its own deadline, and this
137
+ * caller stops waiting for it at its own. */
138
+ private discoveryWithin;
139
+ /**
140
+ * The app's page on the signed-in person's crouter cloud account, where they change or remove what the app may do:
141
+ * the discovery document's `account_app_uri_template` with its one `{client_id}` replaced by the encoded client id.
142
+ * Needs no user or connection; its only request is the shared discovery read. Throws `CrouterError` when the field
143
+ * is absent, does not contain `{client_id}` exactly once, or does not expand to a URL on the issuer's origin.
144
+ */
145
+ accountAppUrl(): Promise<string>;
146
+ /** 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`.
147
+ * `prompt: 'consent'` makes the directory show its consent screen even when the person already approved these scopes. */
148
+ authorizeUrl<const S extends readonly string[]>({ scopes, state, codeVerifier, prompt }: {
149
+ scopes: S;
150
+ state: string;
151
+ codeVerifier?: string;
152
+ prompt?: 'consent';
153
+ }): Promise<{
154
+ url: string;
155
+ codeVerifier: string;
156
+ state: string;
157
+ nonce: AuthorizeNonce<S>;
158
+ }>;
159
+ /**
160
+ * Finish a sign-in that asked for runtime scopes and return the person's `Connection`. A sign-in-only
161
+ * request (`openid`, `openid email`, `openid profile`, or `openid email profile`) gets no connection from the directory; finish it with
162
+ * `exchangeIdentity`, which this method names when it receives such a response.
163
+ */
164
+ exchangeCode({ code, codeVerifier, state, expectedState, nonce }: {
165
+ code: string;
166
+ codeVerifier: string;
167
+ state: string;
168
+ expectedState: string;
169
+ nonce?: string;
170
+ }): Promise<Connection>;
171
+ /**
172
+ * Finish a sign-in-only request (`authorizeUrl` with scopes `openid` or `openid email`) and return the
173
+ * person's verified identity. There is no connection, refresh token, or runtime access on this path.
174
+ * `nonce` is the value `authorizeUrl` returned and is required. A response without an ID token, or with
175
+ * one that fails verification, throws `id_token_invalid`; a response carrying a runtime connection (the
176
+ * request asked for runtime scopes) throws `oauth_exchange_mismatch` and must be finished with `exchangeCode`.
177
+ */
178
+ exchangeIdentity({ code, codeVerifier, state, expectedState, nonce }: {
179
+ code: string;
180
+ codeVerifier: string;
181
+ state: string;
182
+ expectedState: string;
183
+ nonce: string;
184
+ }): Promise<Identity>;
185
+ /**
186
+ * Refresh the store's idle connections once (the app schedules it, for example daily, with `olderThanMs` of 30
187
+ * days). Each refusal goes through `onRefusal` and the connection's removal, and is counted in `refused`.
188
+ * `failures` holds every connection still stored that could not be refreshed: an error other than a refusal,
189
+ * or a refusal whose `onRefusal` threw (`refusalOf(failure.error)` is set and `failure.error.cause` is what the
190
+ * handler threw); the next run retries those. A connection removed before its turn is skipped.
191
+ */
192
+ refreshIdleConnections({ olderThanMs }: {
193
+ olderThanMs: number;
194
+ }): Promise<{
195
+ refreshed: number;
196
+ refused: number;
197
+ failures: RefreshFailure[];
198
+ }>;
199
+ /**
200
+ * Rotate `connection`'s refresh token through the store: under the user's lock it reloads, and when another
201
+ * process already rotated (or the person signed in again) it returns the stored connection without calling the
202
+ * directory. Nothing stored throws `CrouterError` `not_connected`. A refusal runs `onRefusal`, removes the
203
+ * connection, and rejects with the directory's `AuthenticationError` (`refusalOf(error)` set).
204
+ */
205
+ refresh(connection: Connection): Promise<Connection>;
206
+ private rotate;
207
+ /**
208
+ * A runtime client on `userId`'s stored connection; `runtimeUrl` on it names the runtime it calls. Nothing
209
+ * stored throws `CrouterError` `not_connected` (show "sign in again"). The client refreshes through the store;
210
+ * once its connection is refused (and `onRefusal` resolved) it throws that same refusal on every later call
211
+ * without asking the directory again. Called for the user whose `onRefusal` is running, it throws
212
+ * `refusal_handler_reentry`.
213
+ */
214
+ client(userId: string): Promise<ConnectedCrouter>;
215
+ /**
216
+ * Follow the app's background runs until each comes to rest, and hand each ending to `onEnded` (see
217
+ * `RunWatchOptions`). Runs are opened with `client(userId)`, so refusals reach `onRefusal`. Call `start()`.
218
+ */
219
+ watchRuns(options: RunWatchOptions): RunWatcher;
220
+ /**
221
+ * Follow the grants of the people `users()` names, and hand each change (and each user's first reading in this
222
+ * process) to `onChanged` (see `GrantWatchOptions`). Grants are read with `client(userId)`, so refusals reach
223
+ * `onRefusal`. Call `start()`.
224
+ */
225
+ watchGrants(options: GrantWatchOptions): GrantWatcher;
226
+ /** 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. */
227
+ verifyIdToken(idToken: string, { nonce }?: {
228
+ nonce?: string;
229
+ }): Promise<IdTokenClaims>;
230
+ revoke(connection: Connection): Promise<boolean>;
231
+ /** A token request. `deadlineAt` is the whole operation's deadline (discovery included); without one it starts now. */
232
+ private tokenResponse;
233
+ private connectionFrom;
234
+ private post;
235
+ }