atmn 1.1.25 → 2.0.0

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 (117) hide show
  1. package/README.md +120 -0
  2. package/dist/bin.js +23102 -0
  3. package/dist/index.js +2906 -0
  4. package/dist/tsconfig.tsbuildinfo +1 -1
  5. package/package.json +27 -74
  6. package/src/actions/api/callApi.ts +280 -0
  7. package/src/actions/api/registerApiCommands.ts +121 -0
  8. package/src/actions/env/fetchOrgInfo.ts +32 -0
  9. package/src/actions/env/types/orgInfo.ts +18 -0
  10. package/src/actions/env.ts +94 -0
  11. package/src/actions/init/runInit.ts +401 -0
  12. package/src/actions/login/keyless.ts +135 -0
  13. package/src/actions/login.ts +144 -0
  14. package/src/actions/pull/appendPlanVersionFixture.ts +373 -0
  15. package/src/actions/pull/applyPreview.ts +553 -0
  16. package/src/actions/pull/applySettingsPreview.ts +114 -0
  17. package/src/actions/pull/changedFixtureKeys.ts +88 -0
  18. package/src/actions/pull/listSourceFiles.ts +29 -0
  19. package/src/actions/pull/locateFixture.ts +78 -0
  20. package/src/actions/pull/resolveCollectionTarget.ts +198 -0
  21. package/src/actions/pull/rewriteConfig.ts +57 -0
  22. package/src/actions/pull/scaffoldConfig.ts +118 -0
  23. package/src/actions/pull.ts +399 -0
  24. package/src/actions/push/backfillInternalIds.ts +394 -0
  25. package/src/actions/push/deprecatedFields.ts +55 -0
  26. package/src/actions/push.ts +294 -0
  27. package/src/actions/reset/runReset.ts +58 -0
  28. package/src/actions/sandbox/createSandbox.ts +83 -0
  29. package/src/actions/sandbox/deleteSandbox.ts +91 -0
  30. package/src/actions/sandbox/listSandboxes.ts +28 -0
  31. package/src/actions/sandbox/types/sandboxClient.ts +16 -0
  32. package/src/actions/sandbox/useSandbox.ts +157 -0
  33. package/src/actions/sandbox/withSandboxScopeHint.ts +27 -0
  34. package/src/actions/skills/skills.ts +246 -0
  35. package/src/auth/announceAuthorizationUrl.ts +26 -0
  36. package/src/auth/browser/openSystemBrowser.ts +7 -0
  37. package/src/auth/browser/tryOpenBrowser.ts +17 -0
  38. package/src/auth/browser/watchLauncher.ts +46 -0
  39. package/src/auth/buildAuthorizationUrl.ts +36 -0
  40. package/src/auth/callbackPages.ts +126 -0
  41. package/src/auth/createOrgApiKeys.ts +46 -0
  42. package/src/auth/keyless.ts +119 -0
  43. package/src/auth/oauthConfig.ts +63 -0
  44. package/src/auth/runOAuthFlow.ts +230 -0
  45. package/src/auth/types/browserOpener.ts +5 -0
  46. package/src/auth/types/impersonationTokens.ts +23 -0
  47. package/src/auth/types/oauthTokens.ts +13 -0
  48. package/src/auth/types/orgApiKeys.ts +6 -0
  49. package/src/bin.ts +9 -0
  50. package/src/cli.ts +648 -0
  51. package/src/config/configPackageName.ts +9 -0
  52. package/src/config/legacyConfig.ts +25 -0
  53. package/src/config/loadConfig.ts +231 -0
  54. package/src/env/assertSandboxTarget.ts +20 -0
  55. package/src/env/loadEnv.ts +184 -0
  56. package/src/env/resolveTarget.ts +134 -0
  57. package/src/env/sandboxKeyName.ts +18 -0
  58. package/src/generated/apiRoutes.ts +3787 -0
  59. package/src/generated/client.ts +51564 -0
  60. package/src/generated/emit.ts +1163 -0
  61. package/src/generated/emitRuntime.ts +522 -0
  62. package/src/generated/features.ts +146 -0
  63. package/src/generated/labels.ts +29 -0
  64. package/src/generated/licenses.ts +287 -0
  65. package/src/generated/lintRules.ts +2207 -0
  66. package/src/generated/lintRuntime.ts +865 -0
  67. package/src/generated/plans.ts +1628 -0
  68. package/src/generated/referralPrograms.ts +22 -0
  69. package/src/generated/rewards.ts +58 -0
  70. package/src/generated/settings.ts +21 -0
  71. package/src/generated/skills.ts +305 -0
  72. package/src/generated/variants.ts +934 -0
  73. package/src/generated/wire.ts +334 -0
  74. package/src/http/autumnFetch.ts +30 -0
  75. package/src/index.ts +20 -0
  76. package/src/project/chooseConfigDir.ts +41 -0
  77. package/src/project/resolveProject.ts +115 -0
  78. package/src/project/rootMarker.ts +40 -0
  79. package/src/prompt/prompt.ts +186 -0
  80. package/src/prompt/select.ts +162 -0
  81. package/src/render/renderEnv.ts +77 -0
  82. package/src/render/renderPreview.ts +945 -0
  83. package/src/render/renderSandboxes.ts +92 -0
  84. package/src/render/stripTerminalControls.ts +19 -0
  85. package/src/repo/findRepoRoot.ts +79 -0
  86. package/src/surgery/appendPropertyEdit.ts +67 -0
  87. package/src/surgery/appendToArray.ts +87 -0
  88. package/src/surgery/appendToBinding.ts +19 -0
  89. package/src/surgery/appendToCollection.ts +40 -0
  90. package/src/surgery/appendToFixtureArray.ts +71 -0
  91. package/src/surgery/arrayBinding.ts +30 -0
  92. package/src/surgery/deleteFixtureLiteral.ts +81 -0
  93. package/src/surgery/deleteReference.ts +48 -0
  94. package/src/surgery/ensureBuilderImport.ts +65 -0
  95. package/src/surgery/findFixture.ts +238 -0
  96. package/src/surgery/fixtureEdit.ts +156 -0
  97. package/src/surgery/fixtureLocation.ts +32 -0
  98. package/src/surgery/insertCollection.ts +86 -0
  99. package/src/surgery/insertFirstProperty.ts +73 -0
  100. package/src/surgery/patchFixtureProperty.ts +152 -0
  101. package/src/surgery/patchSingletonProperty.ts +221 -0
  102. package/src/surgery/replaceFixture.ts +28 -0
  103. package/src/surgery/setFixtureProperty.ts +55 -0
  104. package/src/surgery/staticFixtureRule.ts +48 -0
  105. package/src/version.ts +5 -0
  106. package/dist/cli.js +0 -146296
  107. package/dist/compose/index.js +0 -122
  108. package/dist/src/compose/builders/builderFunctions.d.ts +0 -84
  109. package/dist/src/compose/builders/rewardFunctions.d.ts +0 -5
  110. package/dist/src/compose/builders/variantFunctions.d.ts +0 -2
  111. package/dist/src/compose/index.d.ts +0 -19
  112. package/dist/src/compose/models/featureModels.d.ts +0 -262
  113. package/dist/src/compose/models/index.d.ts +0 -3
  114. package/dist/src/compose/models/planModels.d.ts +0 -562
  115. package/dist/src/compose/models/rewardModels.d.ts +0 -52
  116. package/dist/src/compose/models/variantModels.d.ts +0 -34
  117. package/readme.md +0 -186
@@ -0,0 +1,46 @@
1
+ import { autumnFetch } from "../http/autumnFetch";
2
+ import { getApiKeysEndpoint } from "./oauthConfig";
3
+ import type { OrgApiKeys } from "./types/orgApiKeys";
4
+
5
+ type ApiKeysResponse = {
6
+ sandbox_key?: string;
7
+ prod_key?: string;
8
+ org_id?: string;
9
+ message?: string;
10
+ error?: string;
11
+ };
12
+
13
+ /** Mint the org's sandbox and production keys from the OAuth access token. */
14
+ export const createOrgApiKeys = async ({
15
+ accessToken,
16
+ backendUrl,
17
+ fetch = autumnFetch,
18
+ }: {
19
+ accessToken: string;
20
+ backendUrl: string;
21
+ fetch?: typeof globalThis.fetch;
22
+ }): Promise<OrgApiKeys> => {
23
+ const response = await fetch(getApiKeysEndpoint({ backendUrl }), {
24
+ method: "POST",
25
+ headers: {
26
+ authorization: `Bearer ${accessToken}`,
27
+ "content-type": "application/json",
28
+ },
29
+ });
30
+
31
+ const body = (await response.json().catch(() => ({}))) as ApiKeysResponse;
32
+
33
+ if (!response.ok) {
34
+ throw new Error(
35
+ body.message ??
36
+ body.error ??
37
+ `Failed to create API keys (${response.status})`,
38
+ );
39
+ }
40
+
41
+ return {
42
+ sandboxKey: body.sandbox_key,
43
+ prodKey: body.prod_key,
44
+ orgId: body.org_id,
45
+ };
46
+ };
@@ -0,0 +1,119 @@
1
+ import { AutumnApiError } from "../generated/client";
2
+ import { autumnFetch } from "../http/autumnFetch";
3
+
4
+ /**
5
+ * Keyless onboarding: a sandbox org with no owner, minted for an agent or a
6
+ * human who has no account yet, and linked to an account later. Not in the
7
+ * spec, so hand-written like the key-minting call rather than generated.
8
+ */
9
+
10
+ export type ProvisionedOrg = {
11
+ organizationId: string;
12
+ organizationSlug: string;
13
+ apiKey: string;
14
+ claimToken: string;
15
+ /** ISO 8601: after this the org can no longer be linked to anyone. */
16
+ claimExpiresAt: string;
17
+ };
18
+
19
+ export type ClaimStarted = {
20
+ claimUrl: string;
21
+ /** ISO 8601: after this the browser claim link stops working. */
22
+ expiresAt: string;
23
+ };
24
+
25
+ type Fetch = typeof globalThis.fetch;
26
+
27
+ const post = async ({
28
+ baseUrl,
29
+ path,
30
+ body,
31
+ secretKey,
32
+ fetch = autumnFetch,
33
+ }: {
34
+ baseUrl: string;
35
+ path: string;
36
+ body: Record<string, unknown>;
37
+ secretKey?: string;
38
+ fetch?: Fetch;
39
+ }): Promise<Record<string, unknown>> => {
40
+ const response = await fetch(`${baseUrl}${path}`, {
41
+ method: "POST",
42
+ headers: {
43
+ "content-type": "application/json",
44
+ ...(secretKey === undefined
45
+ ? {}
46
+ : { authorization: `Bearer ${secretKey}` }),
47
+ },
48
+ body: JSON.stringify(body),
49
+ });
50
+ const text = await response.text();
51
+ const parsed: unknown = text ? JSON.parse(text) : null;
52
+ if (!response.ok)
53
+ throw new AutumnApiError({ status: response.status, body: parsed, path });
54
+ return (parsed ?? {}) as Record<string, unknown>;
55
+ };
56
+
57
+ export const provisionKeylessOrg = async ({
58
+ baseUrl,
59
+ name,
60
+ slug,
61
+ fetch,
62
+ }: {
63
+ baseUrl: string;
64
+ name: string;
65
+ slug: string;
66
+ fetch?: Fetch;
67
+ }): Promise<ProvisionedOrg> => {
68
+ const body = await post({
69
+ baseUrl,
70
+ path: "/agent.provision",
71
+ body: { name: name.slice(0, MAX_NAME_LENGTH), slug },
72
+ fetch,
73
+ });
74
+ return {
75
+ organizationId: String(body.organization_id),
76
+ organizationSlug: String(body.organization_slug),
77
+ apiKey: String(body.api_key),
78
+ claimToken: String(body.claim_token),
79
+ claimExpiresAt: String(body.claim_expires_at),
80
+ };
81
+ };
82
+
83
+ /** Creates and emails a browser claim link for the org `secretKey` belongs to. */
84
+ export const startClaim = async ({
85
+ baseUrl,
86
+ secretKey,
87
+ email,
88
+ fetch,
89
+ }: {
90
+ baseUrl: string;
91
+ secretKey: string;
92
+ email: string;
93
+ fetch?: Fetch;
94
+ }): Promise<ClaimStarted> => {
95
+ const body = await post({
96
+ baseUrl,
97
+ path: "/agent.start_claim",
98
+ body: { email },
99
+ secretKey,
100
+ fetch,
101
+ });
102
+ return {
103
+ claimUrl: String(body.claim_url),
104
+ expiresAt: String(body.expires_at),
105
+ };
106
+ };
107
+
108
+ /** A slug the server accepts: lowercase alphanumerics, `-` or `_` between words. */
109
+ /** The server caps a name and a slug at 100 characters. */
110
+ const MAX_NAME_LENGTH = 100;
111
+
112
+ export const slugFor = (name: string): string =>
113
+ name
114
+ .toLowerCase()
115
+ .replace(/^@[^/]+\//, "")
116
+ .replace(/[^a-z0-9]+/g, "-")
117
+ .replace(/^-+|-+$/g, "")
118
+ .slice(0, MAX_NAME_LENGTH)
119
+ .replace(/-+$/, "") || "autumn";
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The registered Better Auth client for the atmn CLI, carried over from v2 so
3
+ * existing consent records keep working. `resolveTarget` lets `--client-id`
4
+ * or `AUTUMN_CLIENT_ID` replace it.
5
+ */
6
+ export const CLI_CLIENT_ID = "hAWUopQqLnsSwuRgeRzIBzKslwXmQUSr";
7
+
8
+ const OAUTH_PORT_BASE = 31448;
9
+ const OAUTH_PORT_RANGE = 5;
10
+
11
+ /** Callback ports tried in order; every one is registered as a redirect URI. */
12
+ export const OAUTH_PORTS = Array.from(
13
+ { length: OAUTH_PORT_RANGE },
14
+ (_, offset) => OAUTH_PORT_BASE + offset,
15
+ );
16
+
17
+ /**
18
+ * Modern read/write scopes covering everything the CLI reads and everything the
19
+ * minted keys are used for. The server rewrites v2's CRUDL scopes to these.
20
+ * platform:* is what `atmn sandbox create|delete` needs, migrations:write what
21
+ * `atmn reset` needs, and organisation:write what a config's `settings` block
22
+ * needs; a key minted before any was requested has to be re-minted with
23
+ * `atmn login`.
24
+ */
25
+ export const CLI_OAUTH_SCOPES = [
26
+ "organisation:read",
27
+ "organisation:write",
28
+ "customers:read",
29
+ "customers:write",
30
+ "features:read",
31
+ "features:write",
32
+ "plans:read",
33
+ "plans:write",
34
+ "migrations:read",
35
+ "migrations:write",
36
+ "rewards:read",
37
+ "rewards:write",
38
+ "apiKeys:read",
39
+ "apiKeys:write",
40
+ "platform:read",
41
+ "platform:write",
42
+ ] as const;
43
+
44
+ export const getOAuthRedirectUri = ({ port }: { port: number }): string =>
45
+ `http://localhost:${port}/`;
46
+
47
+ export const getAuthorizationEndpoint = ({
48
+ backendUrl,
49
+ }: {
50
+ backendUrl: string;
51
+ }): string => `${backendUrl}/api/auth/oauth2/authorize`;
52
+
53
+ export const getTokenEndpoint = ({
54
+ backendUrl,
55
+ }: {
56
+ backendUrl: string;
57
+ }): string => `${backendUrl}/api/auth/oauth2/token`;
58
+
59
+ export const getApiKeysEndpoint = ({
60
+ backendUrl,
61
+ }: {
62
+ backendUrl: string;
63
+ }): string => `${backendUrl}/cli/api-keys`;
@@ -0,0 +1,230 @@
1
+ import { createServer } from "node:http";
2
+ import {
3
+ generateCodeVerifier,
4
+ generateState,
5
+ type OAuth2Client,
6
+ OAuth2RequestError,
7
+ } from "arctic";
8
+ import {
9
+ buildAuthorizationUrl,
10
+ createOAuthClient,
11
+ } from "./buildAuthorizationUrl";
12
+ import { renderErrorPage, renderSuccessPage } from "./callbackPages";
13
+ import { getTokenEndpoint, OAUTH_PORTS } from "./oauthConfig";
14
+ import { readImpersonationTokens } from "./types/impersonationTokens";
15
+ import type { AuthorizationOutcome } from "./types/oauthTokens";
16
+
17
+ const AUTHORIZATION_TIMEOUT_MS = 5 * 60 * 1000;
18
+
19
+ export type AuthorizationUrlListener = ({
20
+ url,
21
+ }: {
22
+ url: string;
23
+ }) => Promise<void> | void;
24
+
25
+ export type RunOAuthFlowOptions = {
26
+ clientId: string;
27
+ backendUrl: string;
28
+ scopes: readonly string[];
29
+ onAuthorizationUrl: AuthorizationUrlListener;
30
+ };
31
+
32
+ type CallbackOutcome = {
33
+ page: string;
34
+ outcome?: AuthorizationOutcome;
35
+ error?: Error;
36
+ };
37
+
38
+ const failedCallback = ({ message }: { message: string }): CallbackOutcome => ({
39
+ page: renderErrorPage({ message }),
40
+ error: new Error(message),
41
+ });
42
+
43
+ const exchangeAuthorizationCode = async ({
44
+ client,
45
+ backendUrl,
46
+ code,
47
+ codeVerifier,
48
+ }: {
49
+ client: OAuth2Client;
50
+ backendUrl: string;
51
+ code: string;
52
+ codeVerifier: string;
53
+ }): Promise<CallbackOutcome> => {
54
+ try {
55
+ const tokens = await client.validateAuthorizationCode(
56
+ getTokenEndpoint({ backendUrl }),
57
+ code,
58
+ codeVerifier,
59
+ );
60
+ return {
61
+ page: renderSuccessPage(),
62
+ outcome: {
63
+ kind: "oauth",
64
+ tokens: {
65
+ accessToken: tokens.accessToken(),
66
+ tokenType: "Bearer",
67
+ expiresInSeconds: tokens.accessTokenExpiresInSeconds(),
68
+ refreshToken: tokens.hasRefreshToken()
69
+ ? tokens.refreshToken()
70
+ : undefined,
71
+ },
72
+ },
73
+ };
74
+ } catch (error) {
75
+ const message =
76
+ error instanceof OAuth2RequestError
77
+ ? `OAuth error: ${error.code}`
78
+ : "Token exchange failed";
79
+ return failedCallback({ message });
80
+ }
81
+ };
82
+
83
+ const readCallback = async ({
84
+ callbackUrl,
85
+ client,
86
+ backendUrl,
87
+ state,
88
+ codeVerifier,
89
+ }: {
90
+ callbackUrl: URL;
91
+ client: OAuth2Client;
92
+ backendUrl: string;
93
+ state: string;
94
+ codeVerifier: string;
95
+ }): Promise<CallbackOutcome> => {
96
+ const denial = callbackUrl.searchParams.get("error");
97
+ if (denial) {
98
+ return failedCallback({
99
+ message: callbackUrl.searchParams.get("error_description") ?? denial,
100
+ });
101
+ }
102
+ if (callbackUrl.searchParams.get("state") !== state) {
103
+ return failedCallback({ message: "Invalid state — possible CSRF" });
104
+ }
105
+
106
+ // An impersonating dashboard session hands back tokens instead of a code.
107
+ const impersonation = readImpersonationTokens(callbackUrl.searchParams);
108
+ if (impersonation) {
109
+ return {
110
+ page: renderSuccessPage(),
111
+ outcome: { kind: "impersonation", tokens: impersonation },
112
+ };
113
+ }
114
+
115
+ const code = callbackUrl.searchParams.get("code");
116
+ if (!code) return failedCallback({ message: "Missing authorization code" });
117
+
118
+ return await exchangeAuthorizationCode({
119
+ client,
120
+ backendUrl,
121
+ code,
122
+ codeVerifier,
123
+ });
124
+ };
125
+
126
+ /** Resolves null when the port is taken, so the caller can try the next one. */
127
+ const listenForCallback = ({
128
+ port,
129
+ onCallback,
130
+ onListening,
131
+ }: {
132
+ port: number;
133
+ onCallback: ({
134
+ callbackUrl,
135
+ }: {
136
+ callbackUrl: URL;
137
+ }) => Promise<CallbackOutcome>;
138
+ onListening: () => void;
139
+ }): Promise<AuthorizationOutcome | null> =>
140
+ new Promise((resolve, reject) => {
141
+ const server = createServer(async (request, response) => {
142
+ const callbackUrl = new URL(
143
+ request.url ?? "/",
144
+ `http://localhost:${port}`,
145
+ );
146
+ if (callbackUrl.pathname !== "/") {
147
+ response.writeHead(404).end("Not found");
148
+ return;
149
+ }
150
+
151
+ const { page, outcome, error } = await onCallback({ callbackUrl });
152
+ clearTimeout(timeout);
153
+
154
+ // Settle only once the page is flushed — the browser's keep-alive socket
155
+ // would otherwise outlive the command and hang the CLI.
156
+ response.writeHead(200, { "Content-Type": "text/html" }).end(page, () => {
157
+ shutDown();
158
+ if (outcome) resolve(outcome);
159
+ else reject(error ?? new Error("Authorization returned no tokens"));
160
+ });
161
+ });
162
+
163
+ const shutDown = () => {
164
+ server.close();
165
+ server.closeAllConnections();
166
+ };
167
+
168
+ const timeout = setTimeout(() => {
169
+ shutDown();
170
+ reject(new Error("Authorization timed out. Please try again."));
171
+ }, AUTHORIZATION_TIMEOUT_MS);
172
+
173
+ server.once("error", (error: NodeJS.ErrnoException) => {
174
+ clearTimeout(timeout);
175
+ if (error.code === "EADDRINUSE") resolve(null);
176
+ else reject(error);
177
+ });
178
+
179
+ server.listen(port, onListening);
180
+ });
181
+
182
+ /**
183
+ * PKCE authorization against a loopback callback. The URL is handed to the
184
+ * caller the moment the server is listening, so it can be shown and opened.
185
+ */
186
+ export const runOAuthFlow = async ({
187
+ clientId,
188
+ backendUrl,
189
+ scopes,
190
+ onAuthorizationUrl,
191
+ }: RunOAuthFlowOptions): Promise<AuthorizationOutcome> => {
192
+ const codeVerifier = generateCodeVerifier();
193
+ const state = generateState();
194
+
195
+ for (const port of OAUTH_PORTS) {
196
+ const client = createOAuthClient({ clientId, port });
197
+ const authorizationUrl = buildAuthorizationUrl({
198
+ client,
199
+ backendUrl,
200
+ scopes,
201
+ state,
202
+ codeVerifier,
203
+ });
204
+
205
+ const outcome = await listenForCallback({
206
+ port,
207
+ onCallback: ({ callbackUrl }) =>
208
+ readCallback({
209
+ callbackUrl,
210
+ client,
211
+ backendUrl,
212
+ state,
213
+ codeVerifier,
214
+ }),
215
+ onListening: () => {
216
+ void Promise.resolve(
217
+ onAuthorizationUrl({ url: authorizationUrl.toString() }),
218
+ ).catch(() => {
219
+ // Announcing is best-effort; the flow still works if it fails.
220
+ });
221
+ },
222
+ });
223
+
224
+ if (outcome) return outcome;
225
+ }
226
+
227
+ throw new Error(
228
+ `All OAuth callback ports (${OAUTH_PORTS.at(0)}-${OAUTH_PORTS.at(-1)}) are in use.`,
229
+ );
230
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Resolves once a browser was actually launched, and rejects otherwise — the
3
+ * rejection is the only honest headless signal, since a TTY proves nothing.
4
+ */
5
+ export type BrowserOpener = ({ url }: { url: string }) => Promise<void>;
@@ -0,0 +1,23 @@
1
+ /** One-hour OAuth tokens handed back when the dashboard session impersonates a customer. */
2
+ export type ImpersonationTokens = {
3
+ sandboxToken: string;
4
+ liveToken: string;
5
+ /** ISO 8601. */
6
+ expiresAt: string;
7
+ };
8
+
9
+ export const IMPERSONATION_CALLBACK_PARAMS = {
10
+ sandboxToken: "impersonation_sandbox_token",
11
+ liveToken: "impersonation_live_token",
12
+ expiresAt: "impersonation_expires_at",
13
+ } as const;
14
+
15
+ export const readImpersonationTokens = (
16
+ params: URLSearchParams,
17
+ ): ImpersonationTokens | null => {
18
+ const sandboxToken = params.get(IMPERSONATION_CALLBACK_PARAMS.sandboxToken);
19
+ const liveToken = params.get(IMPERSONATION_CALLBACK_PARAMS.liveToken);
20
+ const expiresAt = params.get(IMPERSONATION_CALLBACK_PARAMS.expiresAt);
21
+ if (!sandboxToken || !liveToken || !expiresAt) return null;
22
+ return { sandboxToken, liveToken, expiresAt };
23
+ };
@@ -0,0 +1,13 @@
1
+ import type { ImpersonationTokens } from "./impersonationTokens";
2
+
3
+ export type OAuthTokens = {
4
+ accessToken: string;
5
+ tokenType: "Bearer";
6
+ expiresInSeconds?: number;
7
+ refreshToken?: string;
8
+ };
9
+
10
+ /** What the loopback callback delivered: a grant to exchange, or ready-made tokens. */
11
+ export type AuthorizationOutcome =
12
+ | { kind: "oauth"; tokens: OAuthTokens }
13
+ | { kind: "impersonation"; tokens: ImpersonationTokens };
@@ -0,0 +1,6 @@
1
+ /** Either key can be absent when the org's OAuth client is bound to one env. */
2
+ export type OrgApiKeys = {
3
+ sandboxKey?: string;
4
+ prodKey?: string;
5
+ orgId?: string;
6
+ };
package/src/bin.ts ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "./cli";
3
+
4
+ // The npm entry: node has no import.meta.main, so the run happens here.
5
+ run({ argv: process.argv }).catch((error: unknown) => {
6
+ const message = error instanceof Error ? error.message : String(error);
7
+ process.stderr.write(`${message}\n`);
8
+ process.exit(1);
9
+ });