sst 2.10.4 → 2.11.1

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.
@@ -12,6 +12,10 @@ export declare const bind: (program: Program) => import("yargs").Argv<{
12
12
  role: string | undefined;
13
13
  } & {
14
14
  future: boolean | undefined;
15
+ } & {
16
+ site: boolean | undefined;
17
+ } & {
18
+ script: boolean | undefined;
15
19
  } & {
16
20
  command: (string | number)[] | undefined;
17
21
  }>;
@@ -1,12 +1,23 @@
1
1
  import path from "path";
2
2
  import { VisibleError } from "../../error.js";
3
- class OutdatedMetadataError extends Error {
3
+ class MetadataNotFoundError extends Error {
4
+ }
5
+ class MetadataOutdatedError extends Error {
4
6
  }
5
7
  export const bind = (program) => program
6
8
  .command(["bind <command..>", "env <command..>"], "Bind your app's resources to a command", (yargs) => yargs
9
+ .option("site", {
10
+ type: "boolean",
11
+ describe: "Run in site mode",
12
+ })
13
+ .option("script", {
14
+ type: "boolean",
15
+ describe: "Run in script mode",
16
+ })
7
17
  .array("command")
8
- .example(`sst bind "vitest run"`, "Bind your resources to your tests")
9
- .example(`sst bind "tsx scripts/myscript.ts"`, "Bind your resources to a script"), async (args) => {
18
+ .example(`sst bind vitest run`, "Bind resources to your tests")
19
+ .example(`sst bind next dev`, "Bind resources to your site")
20
+ .example(`sst bind --script next build`, "Bind resources to your site before deployment"), async (args) => {
10
21
  const { spawn } = await import("child_process");
11
22
  const kill = await import("tree-kill");
12
23
  const { useProject } = await import("../../project.js");
@@ -22,6 +33,7 @@ export const bind = (program) => program
22
33
  const project = useProject();
23
34
  const command = args.command?.join(" ");
24
35
  const isSite = await isRunningInSite();
36
+ const mode = args.site ? "site" : args.script ? "script" : "auto";
25
37
  let p;
26
38
  let timer;
27
39
  let siteConfigCache;
@@ -30,22 +42,12 @@ export const bind = (program) => program
30
42
  throw new VisibleError(`Command is required, e.g. sst bind ${isSite ? "next dev" : "vitest run"}`);
31
43
  }
32
44
  // Bind script
33
- if (!isSite) {
45
+ if (args.script || (!isSite && !args.site)) {
34
46
  Logger.debug("Running in script mode.");
35
47
  return await bindScript();
36
48
  }
37
49
  // Bind site
38
- try {
39
- await bindSite("init");
40
- }
41
- catch (e) {
42
- // Bind script (fallback)
43
- if (e instanceof OutdatedMetadataError) {
44
- Colors.line(Colors.warning("Warning: This was deployed with an old version of SST. Run `sst dev` or `sst deploy` to update."));
45
- return await bindScript();
46
- }
47
- throw e;
48
- }
50
+ await bindSite("init");
49
51
  bus.subscribe("stacks.metadata.updated", () => bindSite("metadata_updated"));
50
52
  bus.subscribe("stacks.metadata.deleted", () => bindSite("metadata_updated"));
51
53
  bus.subscribe("config.secret.updated", (payload) => {
@@ -93,7 +95,25 @@ export const bind = (program) => program
93
95
  }
94
96
  async function bindSite(reason) {
95
97
  // Get metadata
96
- const siteMetadata = await getSiteMetadataUntilAvailable();
98
+ let siteMetadata;
99
+ try {
100
+ siteMetadata = await getSiteMetadata();
101
+ }
102
+ catch (e) {
103
+ // unhandled error
104
+ if (!(e instanceof MetadataOutdatedError) &&
105
+ !(e instanceof MetadataNotFoundError)) {
106
+ throw e;
107
+ }
108
+ // ignore error if previously failed to fetch metadata
109
+ if (reason !== "init")
110
+ return;
111
+ // run in script mode
112
+ Colors.line(Colors.warning(e instanceof MetadataOutdatedError
113
+ ? "Warning: This was deployed with an old version of SST. Run `sst dev` or `sst deploy` to update."
114
+ : "Warning: The site has not been deployed. Some resources might not be available."));
115
+ return await bindScript();
116
+ }
97
117
  const siteConfig = await parseSiteMetadata(siteMetadata);
98
118
  // Handle rebind due to metadata updated
99
119
  if (reason === "metadata_updated") {
@@ -103,57 +123,26 @@ export const bind = (program) => program
103
123
  }
104
124
  siteConfigCache = siteConfig;
105
125
  // Assume function's role credentials
106
- if (siteConfig.role) {
107
- const credentials = await assumeSsrRole(siteConfig.role);
108
- if (credentials) {
109
- // refresh crecentials 1 minute before expiration
110
- const expireAt = credentials.Expiration.getTime() - 60000;
111
- clearTimeout(timer);
112
- timer = setTimeout(() => {
113
- Colors.line(`\n`, `Your AWS session is about to expire. Creating a new session and restarting \`${command}\`...`);
114
- bindSite("iam_expired");
115
- }, expireAt - Date.now());
116
- await runCommand({
117
- ...siteConfig.envs,
118
- AWS_ACCESS_KEY_ID: credentials.AccessKeyId,
119
- AWS_SECRET_ACCESS_KEY: credentials.SecretAccessKey,
120
- AWS_SESSION_TOKEN: credentials.SessionToken,
121
- });
122
- return;
123
- }
124
- }
125
126
  // Fallback to use local IAM credentials
127
+ const credentials = (siteConfig.role &&
128
+ (await getLiveIamCredentials(siteConfig.role))) ||
129
+ (await getLocalIamCredentials());
126
130
  await runCommand({
127
131
  ...siteConfig.envs,
128
- ...(await localIamCredentials()),
132
+ ...credentials,
129
133
  });
130
134
  }
131
135
  async function bindScript() {
132
136
  const { Config } = await import("../../config.js");
133
137
  await runCommand({
134
138
  ...(await Config.env()),
135
- ...(await localIamCredentials()),
139
+ ...(await getLocalIamCredentials()),
136
140
  });
137
141
  }
138
- async function getSiteMetadataUntilAvailable() {
139
- const { createSpinner } = await import("../spinner.js");
140
- const spinner = createSpinner({});
141
- while (true) {
142
- const data = await getSiteMetadata();
143
- // Handle site metadata not found
144
- if (!data) {
145
- spinner.start("Make sure `sst dev` is running...");
146
- await new Promise((resolve) => setTimeout(resolve, 1000));
147
- continue;
148
- }
149
- spinner.isSpinning && spinner.stop().clear();
150
- return data;
151
- }
152
- }
153
142
  async function getSiteMetadata() {
154
143
  const { metadata } = await import("../../stacks/metadata.js");
155
144
  const metadataData = await metadata();
156
- return Object.values(metadataData)
145
+ const data = Object.values(metadataData)
157
146
  .flat()
158
147
  .filter((c) => [
159
148
  "StaticSite",
@@ -170,10 +159,14 @@ export const bind = (program) => program
170
159
  if (!c.data.path ||
171
160
  (isSsr && !c.data.server) ||
172
161
  (!isSsr && !c.data.environment)) {
173
- throw new OutdatedMetadataError();
162
+ throw new MetadataOutdatedError();
174
163
  }
175
164
  return (path.resolve(project.paths.root, c.data.path) === process.cwd());
176
165
  });
166
+ if (!data) {
167
+ throw new MetadataNotFoundError();
168
+ }
169
+ return data;
177
170
  }
178
171
  async function parseSiteMetadata(metadata) {
179
172
  const { LambdaClient, GetFunctionCommand } = await import("@aws-sdk/client-lambda");
@@ -194,40 +187,24 @@ export const bind = (program) => program
194
187
  secrets: metadata.data.secrets,
195
188
  };
196
189
  }
197
- async function assumeSsrRole(roleArn) {
198
- const { STSClient, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
199
- const { useAWSClient } = await import("../../credentials.js");
200
- const sts = useAWSClient(STSClient);
201
- const assumeRole = async (duration) => {
202
- const { Credentials: credentials } = await sts.send(new AssumeRoleCommand({
203
- RoleArn: roleArn,
204
- RoleSessionName: "dev-session",
205
- DurationSeconds: duration,
206
- }));
207
- return credentials;
190
+ async function getLiveIamCredentials(roleArn) {
191
+ const credentials = await assumeSsrRole(roleArn);
192
+ if (!credentials)
193
+ return;
194
+ // refresh crecentials 1 minute before expiration
195
+ const expireAt = credentials.Expiration.getTime() - 60000;
196
+ clearTimeout(timer);
197
+ timer = setTimeout(() => {
198
+ Colors.line(`\n`, `Your AWS session is about to expire. Creating a new session and restarting \`${command}\`...`);
199
+ bindSite("iam_expired");
200
+ }, expireAt - Date.now());
201
+ return {
202
+ AWS_ACCESS_KEY_ID: credentials.AccessKeyId,
203
+ AWS_SECRET_ACCESS_KEY: credentials.SecretAccessKey,
204
+ AWS_SESSION_TOKEN: credentials.SessionToken,
208
205
  };
209
- // Assue role with max duration first. This can fail if chaining roles, or if
210
- // the role has a max duration set. If it fails, assume role with 1 hour duration.
211
- let err;
212
- try {
213
- return await assumeRole(43200);
214
- }
215
- catch (e) {
216
- err = e;
217
- }
218
- if (err.name === "ValidationError" &&
219
- err.message.startsWith("The requested DurationSeconds exceeds")) {
220
- try {
221
- return await assumeRole(3600);
222
- }
223
- catch (e) {
224
- err = e;
225
- }
226
- }
227
- Colors.line("Using local IAM credentials since `sst dev` is not running.");
228
- Logger.debug(`Failed to assume ${roleArn}.`, err);
229
206
  }
230
- async function localIamCredentials() {
207
+ async function getLocalIamCredentials() {
231
208
  const { useAWSCredentials } = await import("../../credentials.js");
232
209
  const credentials = await useAWSCredentials();
233
210
  return {
@@ -269,5 +246,38 @@ export const bind = (program) => program
269
246
  return (Object.keys(envs1).length === Object.keys(envs2).length &&
270
247
  Object.keys(envs1).every((key) => envs1[key] === envs2[key]));
271
248
  }
249
+ async function assumeSsrRole(roleArn) {
250
+ const { STSClient, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
251
+ const { useAWSClient } = await import("../../credentials.js");
252
+ const sts = useAWSClient(STSClient);
253
+ const assumeRole = async (duration) => {
254
+ const { Credentials: credentials } = await sts.send(new AssumeRoleCommand({
255
+ RoleArn: roleArn,
256
+ RoleSessionName: "dev-session",
257
+ DurationSeconds: duration,
258
+ }));
259
+ return credentials;
260
+ };
261
+ // Assue role with max duration first. This can fail if chaining roles, or if
262
+ // the role has a max duration set. If it fails, assume role with 1 hour duration.
263
+ let err;
264
+ try {
265
+ return await assumeRole(43200);
266
+ }
267
+ catch (e) {
268
+ err = e;
269
+ }
270
+ if (err.name === "ValidationError" &&
271
+ err.message.startsWith("The requested DurationSeconds exceeds")) {
272
+ try {
273
+ return await assumeRole(3600);
274
+ }
275
+ catch (e) {
276
+ err = e;
277
+ }
278
+ }
279
+ Colors.line("Using local IAM credentials since `sst dev` is not running.");
280
+ Logger.debug(`Failed to assume ${roleArn}.`, err);
281
+ }
272
282
  })
273
283
  .strict(false);
@@ -14,4 +14,6 @@ export declare const list: (program: Program) => import("yargs").Argv<{
14
14
  future: boolean | undefined;
15
15
  } & {
16
16
  format: string | undefined;
17
+ } & {
18
+ fallback: boolean | undefined;
17
19
  }>;
@@ -1,11 +1,19 @@
1
- export const list = (program) => program.command("list [format]", "Fetch all the secrets", (yargs) => yargs.positional("format", {
1
+ export const list = (program) => program.command("list [format]", "Fetch all the secrets", (yargs) => yargs
2
+ .positional("format", {
2
3
  type: "string",
3
4
  choices: ["table", "env", "json"],
4
- }), async (args) => {
5
+ })
6
+ .boolean("fallback"), async (args) => {
5
7
  const { Config } = await import("../../../config.js");
6
8
  const { gray } = await import("colorette");
7
9
  const { Colors } = await import("../../colors.js");
8
- const secrets = await Config.secrets();
10
+ const configSecrets = await Config.secrets();
11
+ const secrets = !args.fallback
12
+ ? configSecrets
13
+ : Object.entries(configSecrets).reduce((carry, [key, value]) => ({
14
+ ...carry,
15
+ ...(!value.value && !!value.fallback ? { [key]: value } : {}),
16
+ }), {});
9
17
  if (Object.entries(secrets).length === 0) {
10
18
  Colors.line("No secrets set");
11
19
  return;
@@ -20,7 +28,7 @@ export const list = (program) => program.command("list [format]", "Fetch all the
20
28
  break;
21
29
  case "env":
22
30
  for (const [key, value] of Object.entries(secrets)) {
23
- console.log(`${key}=${value.value || value.fallback}`);
31
+ console.log(`${key}=${value.value || `${value.fallback} #fallback`}`);
24
32
  }
25
33
  break;
26
34
  case "table":
@@ -42,7 +50,8 @@ export const list = (program) => program.command("list [format]", "Fetch all the
42
50
  const value = secrets[key].value
43
51
  ? secrets[key].value
44
52
  : `${secrets[key].fallback} ${gray("(fallback)")}`;
45
- console.log(`│ ${key.padEnd(keyLen)} ${value.padEnd(valueLen)} │`);
53
+ const colourPadding = secrets[key].value ? 0 : gray("").length;
54
+ console.log(`│ ${key.padEnd(keyLen)} │ ${value.padEnd(valueLen + colourPadding)} │`);
46
55
  });
47
56
  console.log("└".padEnd(keyLen + 3, "─") +
48
57
  "┴" +
@@ -29,7 +29,7 @@ const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
29
29
  export class NextjsSite extends SsrSite {
30
30
  constructor(scope, id, props) {
31
31
  super(scope, id, {
32
- buildCommand: "npx --yes open-next@1.3.x build",
32
+ buildCommand: "npx --yes open-next@^1.3 build",
33
33
  ...props,
34
34
  });
35
35
  this.createWarmer();
@@ -1,7 +1,15 @@
1
1
  import { OidcBasicConfig } from "./oidc.js";
2
- type GoogleConfig = OidcBasicConfig & {
2
+ import { OauthBasicConfig } from "./oauth.js";
3
+ type GooglePrompt = "none" | "consent" | "select_account";
4
+ type GoogleAccessType = "offline" | "online";
5
+ type GoogleConfig = (OauthBasicConfig & {
6
+ mode: "oauth";
7
+ prompt?: GooglePrompt;
8
+ accessType?: GoogleAccessType;
9
+ }) | (OidcBasicConfig & {
3
10
  mode: "oidc";
4
- };
11
+ prompt?: GooglePrompt;
12
+ });
5
13
  export declare function GoogleAdapter(config: GoogleConfig): () => Promise<{
6
14
  type: "success";
7
15
  properties: {
@@ -1,7 +1,19 @@
1
1
  import { Issuer } from "openid-client";
2
2
  import { OidcAdapter } from "./oidc.js";
3
+ import { OauthAdapter } from "./oauth.js";
3
4
  const issuer = await Issuer.discover("https://accounts.google.com");
4
5
  export function GoogleAdapter(config) {
6
+ /* @__PURE__ */
7
+ if (config.mode === "oauth") {
8
+ return OauthAdapter({
9
+ issuer,
10
+ ...config,
11
+ params: {
12
+ ...(config.accessType && { access_type: config.accessType }),
13
+ ...config.params,
14
+ },
15
+ });
16
+ }
5
17
  return OidcAdapter({
6
18
  issuer,
7
19
  scope: "openid email profile",
@@ -1,6 +1,7 @@
1
1
  import { OidcBasicConfig } from "./oidc.js";
2
2
  type MicrosoftConfig = OidcBasicConfig & {
3
3
  mode: "oidc";
4
+ prompt?: "login" | "none" | "consent" | "select_account";
4
5
  };
5
6
  export declare function MicrosoftAdapter(config: MicrosoftConfig): () => Promise<{
6
7
  type: "success";
@@ -12,7 +12,14 @@ export interface OauthBasicConfig {
12
12
  * Various scopes requested for the access token
13
13
  */
14
14
  scope: string;
15
+ /**
16
+ * Determines whether users will be prompted for reauthentication and consent
17
+ */
15
18
  prompt?: string;
19
+ /**
20
+ * Additional parameters to be passed to the authorization endpoint
21
+ */
22
+ params?: Record<string, string>;
16
23
  }
17
24
  export interface OauthConfig extends OauthBasicConfig {
18
25
  issuer: Issuer;
@@ -22,6 +22,7 @@ export const OauthAdapter =
22
22
  code_challenge_method: "S256",
23
23
  state,
24
24
  prompt: config.prompt,
25
+ ...config.params,
25
26
  });
26
27
  useResponse().cookies({
27
28
  auth_code_verifier: code_verifier,
@@ -4,6 +4,10 @@ export interface OidcBasicConfig {
4
4
  * The clientID provided by the third party oauth service
5
5
  */
6
6
  clientID: string;
7
+ /**
8
+ * Determines whether users will be prompted for reauthentication and consent
9
+ */
10
+ prompt?: string;
7
11
  }
8
12
  export interface OidcConfig extends OidcBasicConfig {
9
13
  issuer: Issuer;
@@ -17,6 +17,7 @@ export const OidcAdapter = /* @__PURE__ */ (config) => {
17
17
  response_mode: "form_post",
18
18
  nonce,
19
19
  state,
20
+ prompt: config.prompt,
20
21
  });
21
22
  useResponse().cookies({
22
23
  auth_nonce: nonce,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "sideEffects": false,
3
3
  "name": "sst",
4
- "version": "2.10.4",
4
+ "version": "2.11.1",
5
5
  "bin": {
6
6
  "sst": "cli/sst.js"
7
7
  },