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.
@@ -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
+ };
@@ -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
- /** The target env for the webhook lane; without it webhooks are not pulled. */
71
- webhookEnv?: () => Promise<WebhookEnv>;
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
- webhookEnv,
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
- webhookEnv === undefined
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: string[] = [];
378
- if (webhooks !== undefined) {
379
- const [env, { list }] = webhooks;
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: lists.webhooks as StatedWebhook[] | undefined,
384
- envKey: env.key,
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
- webhookEnv: webhookEnvFor({ target, command }),
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",