atmn 2.0.35 → 2.0.37
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/dist/bin.js +173 -43
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/actions/pull/webhooks/applyWebhooksPull.ts +58 -10
- package/src/actions/pull/webhooks/readWebhookEnvs.ts +41 -0
- package/src/actions/pull/webhooks/webhookPullEnvs.ts +84 -0
- package/src/actions/pull.ts +13 -12
- package/src/cli.ts +13 -1
- package/src/env/sandboxKeyName.ts +6 -0
- package/src/generated/apiRoutes.ts +14 -0
- package/src/generated/skills.ts +10 -10
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { AutumnApiError } from "../../../generated/client";
|
|
2
|
+
import type { RemoteWebhook } from "./types";
|
|
3
|
+
import { ForeignOrgKeyError, type WebhookPullEnv } from "./webhookPullEnvs";
|
|
4
|
+
|
|
5
|
+
const isRejectedKey = (error: unknown): boolean =>
|
|
6
|
+
error instanceof AutumnApiError &&
|
|
7
|
+
(error.status === 401 || error.status === 403);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Lists every env in parallel. A rejected key, or one for another org, skips
|
|
11
|
+
* its env with one warning; any other failure fails the pull.
|
|
12
|
+
*/
|
|
13
|
+
export const readWebhookEnvs = async ({
|
|
14
|
+
envs,
|
|
15
|
+
}: {
|
|
16
|
+
envs: WebhookPullEnv[];
|
|
17
|
+
}): Promise<{
|
|
18
|
+
read: { envKey: string; list: RemoteWebhook[] }[];
|
|
19
|
+
skipped: string[];
|
|
20
|
+
}> => {
|
|
21
|
+
const results = await Promise.all(
|
|
22
|
+
envs.map(async (env) => {
|
|
23
|
+
try {
|
|
24
|
+
const [envKey, { list }] = await Promise.all([
|
|
25
|
+
env.envKey(),
|
|
26
|
+
env.listWebhooks(),
|
|
27
|
+
]);
|
|
28
|
+
return { envKey, list };
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (error instanceof ForeignOrgKeyError)
|
|
31
|
+
return `⚠ webhooks: skipped ${env.label} (${env.keyName} belongs to another org)`;
|
|
32
|
+
if (!isRejectedKey(error)) throw error;
|
|
33
|
+
return `⚠ webhooks: skipped ${env.label} (${env.keyName} was rejected)`;
|
|
34
|
+
}
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
return {
|
|
38
|
+
read: results.filter((result) => typeof result !== "string"),
|
|
39
|
+
skipped: results.filter((result) => typeof result === "string"),
|
|
40
|
+
};
|
|
41
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { SecretKeyName } from "../../../env/resolveTarget";
|
|
2
|
+
import { isSandboxKeyName } from "../../../env/sandboxKeyName";
|
|
3
|
+
import { sandboxSlug } from "../../../generated/sandboxName";
|
|
4
|
+
import type { OrgInfo } from "../../env/types/orgInfo";
|
|
5
|
+
import type { RemoteWebhook } from "./types";
|
|
6
|
+
|
|
7
|
+
/** One env a pull reads webhooks from, through that env's own key. */
|
|
8
|
+
export type WebhookPullEnv = {
|
|
9
|
+
keyName: SecretKeyName;
|
|
10
|
+
/** How a skipped-env warning names it, before its key is known to work. */
|
|
11
|
+
label: string;
|
|
12
|
+
/** The `url` map key; a named sandbox's slug costs one lookup. */
|
|
13
|
+
envKey: () => Promise<string>;
|
|
14
|
+
listWebhooks: () => Promise<{ list: RemoteWebhook[] }>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** A key that works, but for another org than the one the pull targets. */
|
|
18
|
+
export class ForeignOrgKeyError extends Error {}
|
|
19
|
+
|
|
20
|
+
/** A sandbox reports the main org that made it; the main org is its own. */
|
|
21
|
+
const mainOrgIdOf = (info: OrgInfo): string =>
|
|
22
|
+
info.is_sandbox && info.created_by ? info.created_by : info.id;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Every env the loaded env files hold a key for. Live comes last, so when envs
|
|
26
|
+
* disagree on shared fields like `events`, prod's value is the one written.
|
|
27
|
+
*/
|
|
28
|
+
export const webhookPullEnvs = ({
|
|
29
|
+
env = process.env,
|
|
30
|
+
targetKeyName,
|
|
31
|
+
listWebhooks,
|
|
32
|
+
fetchOrgInfo,
|
|
33
|
+
}: {
|
|
34
|
+
env?: Record<string, string | undefined>;
|
|
35
|
+
/** The key the pull targets: every other key must belong to its org. */
|
|
36
|
+
targetKeyName: SecretKeyName;
|
|
37
|
+
listWebhooks: (args: {
|
|
38
|
+
secretKey: string;
|
|
39
|
+
}) => Promise<{ list: RemoteWebhook[] }>;
|
|
40
|
+
fetchOrgInfo: (args: { secretKey: string }) => Promise<OrgInfo>;
|
|
41
|
+
}): WebhookPullEnv[] => {
|
|
42
|
+
const infos = new Map<string, Promise<OrgInfo>>();
|
|
43
|
+
const infoOf = (secretKey: string) => {
|
|
44
|
+
if (!infos.has(secretKey))
|
|
45
|
+
infos.set(secretKey, fetchOrgInfo({ secretKey }));
|
|
46
|
+
return infos.get(secretKey) as Promise<OrgInfo>;
|
|
47
|
+
};
|
|
48
|
+
const targetKey = env[targetKeyName];
|
|
49
|
+
const targetOrg = async () =>
|
|
50
|
+
targetKey === undefined ? undefined : mainOrgIdOf(await infoOf(targetKey));
|
|
51
|
+
const named = Object.keys(env).filter(isSandboxKeyName).sort();
|
|
52
|
+
const keyNames: SecretKeyName[] = [
|
|
53
|
+
"AUTUMN_SECRET_KEY",
|
|
54
|
+
...named,
|
|
55
|
+
"AUTUMN_PROD_SECRET_KEY",
|
|
56
|
+
];
|
|
57
|
+
return keyNames.flatMap((keyName) => {
|
|
58
|
+
const secretKey = env[keyName];
|
|
59
|
+
if (!secretKey) return [];
|
|
60
|
+
const envKey = async () => {
|
|
61
|
+
if (keyName !== targetKeyName) {
|
|
62
|
+
const [info, org] = await Promise.all([infoOf(secretKey), targetOrg()]);
|
|
63
|
+
if (mainOrgIdOf(info) !== org) throw new ForeignOrgKeyError();
|
|
64
|
+
}
|
|
65
|
+
if (keyName === "AUTUMN_SECRET_KEY") return "sandbox";
|
|
66
|
+
if (keyName === "AUTUMN_PROD_SECRET_KEY") return "live";
|
|
67
|
+
return sandboxSlug((await infoOf(secretKey)).name);
|
|
68
|
+
};
|
|
69
|
+
const label =
|
|
70
|
+
keyName === "AUTUMN_SECRET_KEY"
|
|
71
|
+
? "sandbox"
|
|
72
|
+
: keyName === "AUTUMN_PROD_SECRET_KEY"
|
|
73
|
+
? "live"
|
|
74
|
+
: `sandbox ${keyName.slice("AUTUMN_SANDBOX_".length, -"_SECRET_KEY".length).toLowerCase()}`;
|
|
75
|
+
return [
|
|
76
|
+
{
|
|
77
|
+
keyName,
|
|
78
|
+
label,
|
|
79
|
+
envKey,
|
|
80
|
+
listWebhooks: () => listWebhooks({ secretKey }),
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
});
|
|
84
|
+
};
|
package/src/actions/pull.ts
CHANGED
|
@@ -31,13 +31,14 @@ import {
|
|
|
31
31
|
scaffoldConfig,
|
|
32
32
|
} from "./pull/scaffoldConfig";
|
|
33
33
|
import { applyWebhooksPull } from "./pull/webhooks/applyWebhooksPull";
|
|
34
|
+
import { readWebhookEnvs } from "./pull/webhooks/readWebhookEnvs";
|
|
34
35
|
import type { StatedWebhook } from "./pull/webhooks/types";
|
|
36
|
+
import type { WebhookPullEnv } from "./pull/webhooks/webhookPullEnvs";
|
|
35
37
|
import { configSearchDirs } from "./push";
|
|
36
38
|
import {
|
|
37
39
|
backfillInternalIds,
|
|
38
40
|
identityRowsFromCatalog,
|
|
39
41
|
} from "./push/backfillInternalIds";
|
|
40
|
-
import type { WebhookEnv } from "./webhooks/types/webhookEnv";
|
|
41
42
|
|
|
42
43
|
export type PullResult = {
|
|
43
44
|
configPath: string;
|
|
@@ -67,8 +68,8 @@ export type PullOptions = {
|
|
|
67
68
|
yes?: boolean;
|
|
68
69
|
/** Asks where a first pull should put the config; headless by default. */
|
|
69
70
|
prompter?: Prompter;
|
|
70
|
-
/**
|
|
71
|
-
|
|
71
|
+
/** Every env the webhook lane reads, whatever the target; none when absent. */
|
|
72
|
+
webhookEnvs?: () => WebhookPullEnv[];
|
|
72
73
|
};
|
|
73
74
|
|
|
74
75
|
const OVERWRITE_HINT =
|
|
@@ -229,7 +230,7 @@ export const runPull = async ({
|
|
|
229
230
|
overwrite = false,
|
|
230
231
|
yes = false,
|
|
231
232
|
prompter = createPrompter({ interactive: false, write }),
|
|
232
|
-
|
|
233
|
+
webhookEnvs,
|
|
233
234
|
}: PullOptions): Promise<PullResult> => {
|
|
234
235
|
const project = resolveProject({ cwd, configFlag });
|
|
235
236
|
if (overwrite && !yes) {
|
|
@@ -286,9 +287,7 @@ export const runPull = async ({
|
|
|
286
287
|
singletons.settings ?? { config: {} },
|
|
287
288
|
) as Promise<SettingsPreview>,
|
|
288
289
|
// Always listed, like settings: a webhook the config never named is pulled in.
|
|
289
|
-
|
|
290
|
-
? undefined
|
|
291
|
-
: Promise.all([webhookEnv(), client.listWebhooks({})]),
|
|
290
|
+
readWebhookEnvs({ envs: webhookEnvs?.() ?? [] }),
|
|
292
291
|
]);
|
|
293
292
|
|
|
294
293
|
const files = new Map<string, string>();
|
|
@@ -374,15 +373,17 @@ export const runPull = async ({
|
|
|
374
373
|
);
|
|
375
374
|
}
|
|
376
375
|
|
|
377
|
-
const webhookWarnings
|
|
378
|
-
|
|
379
|
-
|
|
376
|
+
const webhookWarnings = [...webhooks.skipped];
|
|
377
|
+
let stated = lists.webhooks as StatedWebhook[] | undefined;
|
|
378
|
+
// Only an env whose list succeeded may remove its urls from the config.
|
|
379
|
+
for (const { envKey, list } of webhooks.read) {
|
|
380
380
|
const applied = applyWebhooksPull({
|
|
381
381
|
pull: { configPath, files },
|
|
382
382
|
remote: list,
|
|
383
|
-
stated
|
|
384
|
-
envKey
|
|
383
|
+
stated,
|
|
384
|
+
envKey,
|
|
385
385
|
});
|
|
386
|
+
stated = applied.stated;
|
|
386
387
|
lines.push(...applied.lines);
|
|
387
388
|
webhookWarnings.push(...applied.warnings);
|
|
388
389
|
unlocated.push(
|
package/src/cli.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
runKeylessLogin,
|
|
15
15
|
} from "./actions/login/keyless";
|
|
16
16
|
import { runPull } from "./actions/pull";
|
|
17
|
+
import { webhookPullEnvs } from "./actions/pull/webhooks/webhookPullEnvs";
|
|
17
18
|
import { pushExitCode, runPush } from "./actions/push";
|
|
18
19
|
import { runReset } from "./actions/reset/runReset";
|
|
19
20
|
import { runSandboxCreate } from "./actions/sandbox/createSandbox";
|
|
@@ -500,7 +501,18 @@ Linking a keyless org to an account:
|
|
|
500
501
|
overwrite: options.overwrite === true,
|
|
501
502
|
yes: options.yes === true,
|
|
502
503
|
prompter: prompterFor({ command }),
|
|
503
|
-
|
|
504
|
+
webhookEnvs: () =>
|
|
505
|
+
webhookPullEnvs({
|
|
506
|
+
targetKeyName: target.secretKeyName,
|
|
507
|
+
listWebhooks: ({ secretKey }) =>
|
|
508
|
+
createClient({
|
|
509
|
+
secretKey,
|
|
510
|
+
fetch: autumnFetch,
|
|
511
|
+
...(target.baseUrl ? { baseUrl: target.baseUrl } : {}),
|
|
512
|
+
}).listWebhooks({}),
|
|
513
|
+
fetchOrgInfo: ({ secretKey }) =>
|
|
514
|
+
fetchOrgInfo({ baseUrl: targetBaseUrl({ target }), secretKey }),
|
|
515
|
+
}),
|
|
504
516
|
});
|
|
505
517
|
writeStaleSkillsHint({ command });
|
|
506
518
|
},
|
|
@@ -16,3 +16,9 @@ export const sandboxKeyName = ({
|
|
|
16
16
|
|
|
17
17
|
/** The env var `atmn sandbox create --use` writes to pin a target sandbox. */
|
|
18
18
|
export const SANDBOX_PIN_NAME = "AUTUMN_SANDBOX_ID";
|
|
19
|
+
|
|
20
|
+
const SANDBOX_KEY_NAME = /^AUTUMN_SANDBOX_[A-Z0-9_]+_SECRET_KEY$/;
|
|
21
|
+
|
|
22
|
+
/** Whether an env var is a named sandbox's key, as `sandboxKeyName` writes it. */
|
|
23
|
+
export const isSandboxKeyName = (name: string): name is SandboxSecretKeyName =>
|
|
24
|
+
SANDBOX_KEY_NAME.test(name);
|
|
@@ -2924,6 +2924,13 @@ export const API_ROUTES: readonly ApiRoute[] = [
|
|
|
2924
2924
|
required: false,
|
|
2925
2925
|
description: "Filter events by time range",
|
|
2926
2926
|
},
|
|
2927
|
+
{
|
|
2928
|
+
name: "range",
|
|
2929
|
+
type: "string",
|
|
2930
|
+
required: false,
|
|
2931
|
+
description:
|
|
2932
|
+
"Preset time range to filter events by, resolved the same way as in events.aggregate. Cannot be combined with custom_range",
|
|
2933
|
+
},
|
|
2927
2934
|
],
|
|
2928
2935
|
},
|
|
2929
2936
|
{
|
|
@@ -2981,6 +2988,13 @@ export const API_ROUTES: readonly ApiRoute[] = [
|
|
|
2981
2988
|
description:
|
|
2982
2989
|
"Custom time range to aggregate events for. If provided, range must not be provided",
|
|
2983
2990
|
},
|
|
2991
|
+
{
|
|
2992
|
+
name: "timezone",
|
|
2993
|
+
type: "string",
|
|
2994
|
+
required: false,
|
|
2995
|
+
description:
|
|
2996
|
+
'IANA timezone (e.g. "America/New_York") that day, week and month bins are aligned to. Hour bins are always UTC. Defaults to UTC',
|
|
2997
|
+
},
|
|
2984
2998
|
{
|
|
2985
2999
|
name: "filter_by",
|
|
2986
3000
|
type: "json",
|