serverless-offline 13.5.1 → 13.7.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.
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
- "dedicatedTo": "Blue, a great migrating bird.",
3
2
  "name": "serverless-offline",
4
- "version": "13.5.1",
3
+ "version": "13.7.0",
5
4
  "description": "Emulate AWS λ and API Gateway locally when developing your Serverless project",
6
5
  "license": "MIT",
7
6
  "exports": {
@@ -77,10 +76,10 @@
77
76
  ]
78
77
  },
79
78
  "dependencies": {
80
- "@aws-sdk/client-lambda": "^3.509.0",
79
+ "@aws-sdk/client-lambda": "^3.636.0",
81
80
  "@hapi/boom": "^10.0.1",
82
81
  "@hapi/h2o2": "^10.0.4",
83
- "@hapi/hapi": "^21.3.3",
82
+ "@hapi/hapi": "^21.3.10",
84
83
  "array-unflat-js": "^0.1.3",
85
84
  "boxen": "^7.1.1",
86
85
  "chalk": "^5.3.0",
@@ -89,31 +88,31 @@
89
88
  "fs-extra": "^11.2.0",
90
89
  "is-wsl": "^3.1.0",
91
90
  "java-invoke-local": "0.0.6",
92
- "jose": "^5.2.1",
91
+ "jose": "^5.7.0",
93
92
  "js-string-escape": "^1.0.1",
94
- "jsonpath-plus": "^8.0.0",
93
+ "jsonpath-plus": "^9.0.0",
95
94
  "jsonschema": "^1.4.1",
96
95
  "jszip": "^3.10.1",
97
- "luxon": "^3.4.4",
96
+ "luxon": "^3.5.0",
98
97
  "node-schedule": "^2.1.1",
99
98
  "p-memoize": "^7.1.1",
100
99
  "velocityjs": "^2.0.6",
101
- "ws": "^8.16.0"
100
+ "ws": "^8.18.0"
102
101
  },
103
102
  "devDependencies": {
104
103
  "@istanbuljs/esm-loader-hook": "^0.2.0",
105
- "archiver": "^6.0.1",
106
- "commit-and-tag-version": "^12.2.0",
107
- "eslint": "^8.56.0",
104
+ "archiver": "^7.0.1",
105
+ "commit-and-tag-version": "^12.4.1",
106
+ "eslint": "^8.57.0",
108
107
  "eslint-config-airbnb-base": "^15.0.0",
109
108
  "eslint-config-prettier": "^9.1.0",
110
109
  "eslint-plugin-import": "^2.29.1",
111
- "eslint-plugin-prettier": "^5.1.3",
112
- "eslint-plugin-unicorn": "^51.0.1",
113
- "mocha": "^10.3.0",
114
- "nyc": "^15.1.0",
115
- "prettier": "^3.2.5",
116
- "serverless": "^3.38.0"
110
+ "eslint-plugin-prettier": "^5.2.1",
111
+ "eslint-plugin-unicorn": "^54.0.0",
112
+ "mocha": "^10.7.3",
113
+ "nyc": "^17.0.0",
114
+ "prettier": "^3.3.3",
115
+ "serverless": "^3.2.0"
117
116
  },
118
117
  "peerDependencies": {
119
118
  "serverless": "^3.2.0"
@@ -1,5 +1,6 @@
1
1
  import process, { exit } from "node:process"
2
2
  import { log, setLogUtils } from "./utils/log.js"
3
+ import logSponsor from "./utils/logSponsor.js"
3
4
  import {
4
5
  commandOptions,
5
6
  CUSTOM_OPTION,
@@ -64,6 +65,12 @@ export default class ServerlessOffline {
64
65
  async start() {
65
66
  this.#mergeOptions()
66
67
 
68
+ if (this.#cliOptions.noSponsor) {
69
+ log.notice()
70
+ } else {
71
+ logSponsor()
72
+ }
73
+
67
74
  const {
68
75
  albEvents,
69
76
  httpEvents,
@@ -278,13 +285,11 @@ export default class ServerlessOffline {
278
285
  origin: this.#options.corsAllowOrigin,
279
286
  }
280
287
 
281
- log.notice()
282
288
  log.notice(
283
289
  `Starting Offline at stage ${
284
290
  this.#options.stage || provider.stage
285
291
  } ${gray(`(${this.#options.region || provider.region})`)}`,
286
292
  )
287
- log.notice()
288
293
  log.debug("options:", this.#options)
289
294
  }
290
295
 
@@ -89,6 +89,10 @@ export default {
89
89
  type: "boolean",
90
90
  usage: "Don't prepend http routes with the stage.",
91
91
  },
92
+ noSponsor: {
93
+ type: "boolean",
94
+ usage: "Remove sponsor message from the output.",
95
+ },
92
96
  noTimeout: {
93
97
  shortcut: "t",
94
98
  type: "boolean",
@@ -31,6 +31,7 @@ export const supportedRuntimesArchitecture = {
31
31
  provided: [X86_64],
32
32
  dotnet6: [ARM64, X86_64],
33
33
  "provided.al2": [ARM64, X86_64],
34
+ "provided.al2023": [ARM64, X86_64],
34
35
  }
35
36
 
36
37
  // GO
@@ -48,7 +49,11 @@ export const supportedNodejs = new Set([
48
49
  ])
49
50
 
50
51
  // PROVIDED
51
- export const supportedProvided = new Set(["provided", "provided.al2"])
52
+ export const supportedProvided = new Set([
53
+ "provided",
54
+ "provided.al2",
55
+ "provided.al2023",
56
+ ])
52
57
 
53
58
  // PYTHON
54
59
  export const supportedPython = new Set([
@@ -267,6 +267,8 @@ export default class HttpServer {
267
267
  override: false,
268
268
  })
269
269
 
270
+ response.headers = headers
271
+
270
272
  if (typeof result === "string") {
271
273
  response.source = stringify(result)
272
274
  } else if (result && result.body !== undefined) {
@@ -1,7 +1,8 @@
1
1
  function parseResource(resource) {
2
- const [, region, accountId, restApiId, path] = resource.match(
3
- /arn:aws:execute-api:(.*?):(.*?):(.*?)\/(.*)/,
4
- )
2
+ const [, region = "*", accountId = "*", restApiId = "*", path = "*"] =
3
+ resource.match(
4
+ /arn:aws:execute-api:([^\s:]+)(?::([^\s:]+))?(?::([^\s/:]+))?(?:\/(.*))?/,
5
+ )
5
6
 
6
7
  return {
7
8
  accountId,
@@ -26,10 +27,6 @@ export default function authMatchPolicyResource(policyResource, resource) {
26
27
  return true
27
28
  }
28
29
 
29
- if (policyResource === "arn:aws:execute-api:*:*:*") {
30
- return true
31
- }
32
-
33
30
  if (policyResource.includes("*") || policyResource.includes("?")) {
34
31
  // Policy contains a wildcard resource
35
32
 
@@ -61,7 +58,7 @@ export default function authMatchPolicyResource(policyResource, resource) {
61
58
  // for the requested resource and the resource defined in the policy
62
59
  // Need to create a regex replacing ? with one character and * with any number of characters
63
60
  const regExp = new RegExp(
64
- parsedPolicyResource.path.replaceAll("*", ".*").replaceAll("?", "."),
61
+ `${parsedPolicyResource.path.replaceAll("*", ".*").replaceAll("?", ".")}$`,
65
62
  )
66
63
 
67
64
  return regExp.test(parsedResource.path)
@@ -295,7 +295,7 @@ export default class HttpServer {
295
295
  return null
296
296
  }
297
297
 
298
- const authFunctionName = this.#extractAuthFunctionName(endpoint)
298
+ let authFunctionName = this.#extractAuthFunctionName(endpoint)
299
299
 
300
300
  if (!authFunctionName) {
301
301
  return null
@@ -303,16 +303,32 @@ export default class HttpServer {
303
303
 
304
304
  log.notice(`Configuring Authorization: ${path} ${authFunctionName}`)
305
305
 
306
+ const standardFunctionExists =
307
+ this.#serverless.service.functions &&
308
+ this.#serverless.service.functions[authFunctionName]
309
+ const serverlessAuthorizerOptions =
310
+ this.#serverless.service.provider.httpApi &&
311
+ this.#serverless.service.provider.httpApi.authorizers &&
312
+ this.#serverless.service.provider.httpApi.authorizers[authFunctionName]
313
+
314
+ if (
315
+ !standardFunctionExists &&
316
+ endpoint.isHttpApi &&
317
+ serverlessAuthorizerOptions &&
318
+ serverlessAuthorizerOptions.functionName
319
+ ) {
320
+ log.notice(
321
+ `Redirecting authorizer function: ${authFunctionName} to ${serverlessAuthorizerOptions.functionName}`,
322
+ )
323
+ authFunctionName = serverlessAuthorizerOptions.functionName
324
+ }
325
+
306
326
  const authFunction = this.#serverless.service.getFunction(authFunctionName)
307
327
 
308
328
  if (!authFunction) {
309
329
  log.error(`Authorization function ${authFunctionName} does not exist`)
310
330
  return null
311
331
  }
312
- const serverlessAuthorizerOptions =
313
- this.#serverless.service.provider.httpApi &&
314
- this.#serverless.service.provider.httpApi.authorizers &&
315
- this.#serverless.service.provider.httpApi.authorizers[authFunctionName]
316
332
 
317
333
  const authorizerOptions = {
318
334
  enableSimpleResponses:
@@ -326,7 +342,8 @@ export default class HttpServer {
326
342
  ? serverlessAuthorizerOptions?.payloadVersion || "2.0"
327
343
  : "1.0",
328
344
  resultTtlInSeconds:
329
- serverlessAuthorizerOptions?.resultTtlInSeconds || "300",
345
+ serverlessAuthorizerOptions?.resultTtlInSeconds ?? "300",
346
+ type: endpoint.isHttpApi ? serverlessAuthorizerOptions?.type : undefined,
330
347
  }
331
348
 
332
349
  if (
@@ -339,11 +356,10 @@ export default class HttpServer {
339
356
  return null
340
357
  }
341
358
 
342
- if (typeof endpoint.authorizer === "string") {
343
- authorizerOptions.name = authFunctionName
344
- } else {
359
+ if (typeof endpoint.authorizer !== "string") {
345
360
  assign(authorizerOptions, endpoint.authorizer)
346
361
  }
362
+ authorizerOptions.name = authFunctionName
347
363
 
348
364
  if (
349
365
  !authorizerOptions.identitySource &&
@@ -443,7 +459,7 @@ export default class HttpServer {
443
459
  request.payload = request.payload && request.payload.toString(encoding)
444
460
  request.rawPayload = request.payload
445
461
 
446
- // incomming request message
462
+ // incoming request message
447
463
  log.notice()
448
464
 
449
465
  log.notice()
@@ -143,6 +143,8 @@ export default function createAuthScheme(authorizerOptions, provider, lambda) {
143
143
  event = {
144
144
  ...event,
145
145
  identitySource: [finalAuthorization],
146
+ pathParameters: nullIfEmpty(pathParams),
147
+ queryStringParameters: parseQueryStringParameters(url),
146
148
  rawPath: request.path,
147
149
  rawQueryString: getRawQueryParams(url),
148
150
  requestContext: {
@@ -5,7 +5,7 @@ import { log } from "../../utils/log.js"
5
5
  const { isArray } = Array
6
6
  const { now } = Date
7
7
 
8
- export default function createAuthScheme(jwtOptions) {
8
+ export default function createJWTAuthScheme(jwtOptions) {
9
9
  const authorizerName = jwtOptions.name
10
10
 
11
11
  const identitySourceMatch = /^\$request.header.((?:\w+-?)+\w+)$/.exec(
@@ -43,7 +43,7 @@ export default function createAuthScheme(jwtOptions) {
43
43
  return Boom.unauthorized("JWT Token expired")
44
44
  }
45
45
 
46
- const { aud, iss, scope, client_id: clientId } = claims
46
+ const { aud, iss, scope, scp, client_id: clientId } = claims
47
47
  if (iss !== jwtOptions.issuerUrl) {
48
48
  log.notice(`JWT Token not from correct issuer url`)
49
49
 
@@ -68,13 +68,13 @@ export default function createAuthScheme(jwtOptions) {
68
68
 
69
69
  let scopes = null
70
70
  if (jwtOptions.scopes && jwtOptions.scopes.length > 0) {
71
- if (!scope) {
71
+ if (!scope && !scp) {
72
72
  log.notice(`JWT Token missing valid scope`)
73
73
 
74
74
  return Boom.forbidden("JWT Token missing valid scope")
75
75
  }
76
76
 
77
- scopes = scope.split(" ")
77
+ scopes = scp || scope.split(" ")
78
78
  if (scopes.every((s) => !jwtOptions.scopes.includes(s))) {
79
79
  log.notice(`JWT Token missing valid scope`)
80
80
 
@@ -85,7 +85,6 @@ export default function createAuthScheme(jwtOptions) {
85
85
  log.notice(`JWT Token validated`)
86
86
 
87
87
  // Set the credentials for the rest of the pipeline
88
- // return resolve(
89
88
  return h.authenticated({
90
89
  credentials: {
91
90
  claims,
@@ -134,8 +134,8 @@ export default class LambdaProxyIntegrationEvent {
134
134
  if (token) {
135
135
  try {
136
136
  claims = decodeJwt(token)
137
- if (claims.scope) {
138
- scopes = claims.scope.split(" ")
137
+ if (claims.scp || claims.scope) {
138
+ scopes = claims.scp || claims.scope.split(" ")
139
139
  // In AWS HTTP Api the scope property is removed from the decoded JWT
140
140
  // I'm leaving this property because I'm not sure how all of the authorizers
141
141
  // for AWS REST Api handle JWT.
@@ -120,8 +120,8 @@ export default class LambdaProxyIntegrationEventV2 {
120
120
  if (token) {
121
121
  try {
122
122
  claims = decodeJwt(token)
123
- if (claims.scope) {
124
- scopes = claims.scope.split(" ")
123
+ if (claims.scp || claims.scope) {
124
+ scopes = claims.scp || claims.scope.split(" ")
125
125
  // In AWS HTTP Api the scope property is removed from the decoded JWT
126
126
  // I'm leaving this property because I'm not sure how all of the authorizers
127
127
  // for AWS REST Api handle JWT.
@@ -10,7 +10,7 @@ const { assign, entries, fromEntries } = Object
10
10
 
11
11
  function escapeJavaScript(x) {
12
12
  if (typeof x === "string") {
13
- return jsEscapeString(x).replaceAll("\\n", "\n") // See #26,
13
+ return jsEscapeString(x).replaceAll(String.raw`\n`, "\n") // See #26,
14
14
  }
15
15
 
16
16
  if (isPlainObject(x)) {
@@ -62,7 +62,7 @@ export default class HttpServer {
62
62
  .listFunctionNames()
63
63
  .map(
64
64
  (functionName) =>
65
- ` * ${funcNamePairs[functionName]}: ${functionName}`,
65
+ ` * ${funcNamePairs[functionName]}: ${functionName}`,
66
66
  ),
67
67
  ].join("\n"),
68
68
  )
@@ -73,9 +73,7 @@ export default class HttpServer {
73
73
  .listFunctionNames()
74
74
  .map(
75
75
  (functionName) =>
76
- ` * ${
77
- invRoute.method
78
- } ${basePath}${invRoute.path.replace(
76
+ ` * ${invRoute.method} ${basePath}${invRoute.path.replace(
79
77
  "{functionName}",
80
78
  functionName,
81
79
  )}`,
@@ -90,7 +88,7 @@ export default class HttpServer {
90
88
  .listFunctionNames()
91
89
  .map(
92
90
  (functionName) =>
93
- ` * ${
91
+ ` * ${
94
92
  invAsyncRoute.method
95
93
  } ${basePath}${invAsyncRoute.path.replace(
96
94
  "{functionName}",
@@ -1,3 +1,6 @@
1
+ /* eslint-disable unicorn/no-abusive-eslint-disable */
2
+ /* eslint-disable */
3
+
1
4
  "use strict"
2
5
 
3
6
  const { pathToFileURL } = require("node:url")
@@ -20,7 +23,7 @@ const { pathToFileURL } = require("node:url")
20
23
  "Errors.js": function (exports2, module2) {
21
24
  "use strict"
22
25
 
23
- const util = require("util")
26
+ const util = require("node:util")
24
27
  function _isError(obj) {
25
28
  return (
26
29
  obj &&
@@ -53,7 +56,7 @@ const { pathToFileURL } = require("node:url")
53
56
  errorMessage: error.toString(),
54
57
  trace: [],
55
58
  }
56
- } catch (_err) {
59
+ } catch {
57
60
  return {
58
61
  errorType: "handled",
59
62
  errorMessage:
@@ -67,7 +70,7 @@ const { pathToFileURL } = require("node:url")
67
70
  return ` ${JSON.stringify(error, (_k, v) =>
68
71
  _withEnumerableProperties(v),
69
72
  )}`
70
- } catch (err) {
73
+ } catch {
71
74
  return ` ${JSON.stringify(toRapidResponse(error))}`
72
75
  }
73
76
  }
@@ -118,9 +121,9 @@ const { pathToFileURL } = require("node:url")
118
121
  return 0
119
122
  }
120
123
  try {
121
- const verbosity = parseInt(process.env[EnvVarName])
124
+ const verbosity = Number.parseInt(process.env[EnvVarName])
122
125
  return verbosity < 0 ? 0 : verbosity > 3 ? 3 : verbosity
123
- } catch (_) {
126
+ } catch {
124
127
  return 0
125
128
  }
126
129
  })()
@@ -128,17 +131,17 @@ const { pathToFileURL } = require("node:url")
128
131
  return {
129
132
  verbose() {
130
133
  if (Verbosity >= 1) {
131
- console.log.apply(null, [Tag, category, ...arguments])
134
+ Reflect.apply(console.log, null, [Tag, category, ...arguments])
132
135
  }
133
136
  },
134
137
  vverbose() {
135
138
  if (Verbosity >= 2) {
136
- console.log.apply(null, [Tag, category, ...arguments])
139
+ Reflect.apply(console.log, null, [Tag, category, ...arguments])
137
140
  }
138
141
  },
139
142
  vvverbose() {
140
143
  if (Verbosity >= 3) {
141
- console.log.apply(null, [Tag, category, ...arguments])
144
+ Reflect.apply(console.log, null, [Tag, category, ...arguments])
142
145
  }
143
146
  },
144
147
  }
@@ -166,8 +169,8 @@ const { pathToFileURL } = require("node:url")
166
169
  module2.exports.HttpResponseStream = HttpResponseStream2
167
170
  },
168
171
  })
169
- const path = require("path")
170
- const fs = require("fs")
172
+ const path = require("node:path")
173
+ const fs = require("node:fs")
171
174
  const {
172
175
  HandlerNotFound,
173
176
  MalformedHandlerName,
@@ -306,7 +309,10 @@ const { pathToFileURL } = require("node:url")
306
309
  if (e instanceof SyntaxError) {
307
310
  throw new UserCodeSyntaxError(e)
308
311
  } else if (e.code !== void 0 && e.code === "MODULE_NOT_FOUND") {
309
- verbose("globalPaths", JSON.stringify(require("module").globalPaths))
312
+ verbose(
313
+ "globalPaths",
314
+ JSON.stringify(require("node:module").globalPaths),
315
+ )
310
316
  throw new ImportModuleError(e)
311
317
  } else {
312
318
  throw e
@@ -322,7 +328,7 @@ const { pathToFileURL } = require("node:url")
322
328
  }
323
329
  function _isHandlerStreaming(handler) {
324
330
  if (
325
- typeof handler[HANDLER_STREAMING] === "undefined" ||
331
+ handler[HANDLER_STREAMING] === undefined ||
326
332
  handler[HANDLER_STREAMING] === null ||
327
333
  handler[HANDLER_STREAMING] === false
328
334
  ) {
@@ -1,6 +1,7 @@
1
1
  # copy/pasted entirely as is from:
2
2
  # https://github.com/serverless/serverless/blob/v1.50.0/lib/plugins/aws/invokeLocal/invoke.py
3
3
 
4
+ import base64
4
5
  import subprocess
5
6
  import argparse
6
7
  import json
@@ -10,6 +11,7 @@ import os
10
11
  from time import strftime, time
11
12
  from importlib import import_module
12
13
 
14
+
13
15
  class FakeLambdaContext(object):
14
16
  def __init__(self, name='Fake', version='LATEST', timeout=6, **kwargs):
15
17
  self.name = name
@@ -102,5 +104,9 @@ if __name__ == '__main__':
102
104
  '__offline_payload__': result
103
105
  }
104
106
 
107
+ if isinstance(result['body'], bytes):
108
+ data['__offline_payload__']['body'] = base64.b64encode(result['body']).decode('utf-8')
109
+ data['isBase64Encoded'] = True
110
+
105
111
  sys.stdout.write(json.dumps(data))
106
112
  sys.stdout.write('\n')
@@ -21,13 +21,20 @@ export { generateAlbHapiPath } from "./generateHapiPath.js"
21
21
  const { isArray } = Array
22
22
  const { keys } = Object
23
23
 
24
+ const possibleBinaryContentTypes = [
25
+ "application/octet-stream",
26
+ "multipart/form-data",
27
+ ]
28
+
24
29
  // Detect the toString encoding from the request headers content-type
25
30
  // enhance if further content types need to be non utf8 encoded.
26
31
  export function detectEncoding(request) {
27
32
  const contentType = request.headers["content-type"]
28
33
 
29
34
  return typeof contentType === "string" &&
30
- contentType.includes("multipart/form-data")
35
+ possibleBinaryContentTypes.some((possibleBinaryContentType) =>
36
+ contentType.includes(possibleBinaryContentType),
37
+ )
31
38
  ? "binary"
32
39
  : "utf8"
33
40
  }
@@ -10,8 +10,7 @@ import {
10
10
  yellow,
11
11
  } from "../config/colors.js"
12
12
 
13
- const { max } = Math
14
-
13
+ const post = "POST"
15
14
  const colorMethodMapping = new Map([
16
15
  ["DELETE", red],
17
16
  ["GET", dodgerblue],
@@ -28,13 +27,13 @@ function logRoute(method, server, path, maxLength, dimPath = false) {
28
27
  const methodColor = colorMethodMapping.get(method) ?? peachpuff
29
28
  const methodFormatted = method.padEnd(maxLength, " ")
30
29
 
31
- return `${methodColor(methodFormatted)} ${yellow.dim("|")} ${gray.dim(
32
- server,
33
- )}${dimPath ? gray.dim(path) : lime(path)}`
30
+ return `${methodColor(methodFormatted)} ${yellow.dim("|")} ${gray.dim(server)}${dimPath ? gray.dim(path) : lime(path)}`
34
31
  }
35
32
 
36
33
  function getMaxHttpMethodNameLength(routeInfo) {
37
- return max(...routeInfo.map(({ method }) => method.length))
34
+ return Math.max(
35
+ ...routeInfo.map(({ method }) => Math.max(method.length, post.length)),
36
+ )
38
37
  }
39
38
 
40
39
  export default function logRoutes(routeInfo) {
@@ -55,7 +54,7 @@ export default function logRoutes(routeInfo) {
55
54
  // eslint-disable-next-line prefer-template
56
55
  logRoute(method, server, path, maxLength) +
57
56
  "\n" +
58
- logRoute("POST", server, invokePath, maxLength, true),
57
+ logRoute(post, server, invokePath, maxLength, true),
59
58
  )
60
59
  .join("\n"),
61
60
  boxenOptions,
@@ -0,0 +1,69 @@
1
+ /* eslint-disable no-use-before-define */
2
+ /* eslint-disable no-console */
3
+ import process from "node:process"
4
+
5
+ import boxen from "boxen"
6
+ import { gray, dodgerblue } from "../config/colors.js"
7
+
8
+ const boxenOptions = {
9
+ borderColor: "blue",
10
+ margin: 1,
11
+ padding: 1,
12
+ }
13
+
14
+ // Promotion starts on August 22, 2024
15
+ const startAt = new Date("2024-08-22T00:00:00.000Z")
16
+ // By October 22, 2024, the promotion will be displayed to 100% of users
17
+ const endAt = new Date("2024-10-22T00:00:00.000Z")
18
+ const nDays = diffDays(startAt, endAt)
19
+
20
+ function logSponsor() {
21
+ if (!shouldDisplaySponsor()) {
22
+ console.log()
23
+
24
+ return
25
+ }
26
+
27
+ console.log(
28
+ boxen(
29
+ `Sponsored by ${dodgerblue("Arccode, the RPG for developers")}\nhttps://arccode.dev?ref=so\n${gray.dim(
30
+ "Disable with --noSponsor",
31
+ )}`,
32
+ boxenOptions,
33
+ ),
34
+ )
35
+ }
36
+
37
+ // Display the message progressively over time to 100% of users
38
+ function shouldDisplaySponsor() {
39
+ const ratio = diffDays(startAt, new Date()) / nDays
40
+
41
+ if (ratio >= 1) return true
42
+
43
+ try {
44
+ const nonce = Number(
45
+ encodeStringToNumber(process.cwd()).toString().padStart(2, "0").slice(-2),
46
+ )
47
+
48
+ return nonce <= ratio * 100
49
+ } catch {
50
+ //
51
+ }
52
+
53
+ return false
54
+ }
55
+
56
+ function encodeStringToNumber(string) {
57
+ let sum = 0
58
+
59
+ for (let i = 0; i < string.length; i += 1) {
60
+ sum += Number(string.codePointAt(i).toString(10))
61
+ }
62
+
63
+ return sum
64
+ }
65
+
66
+ function diffDays(a, b) {
67
+ return Math.round((b - a) / (1000 * 60 * 60 * 24))
68
+ }
69
+ export default logSponsor