@vercel/sandbox 0.0.17 → 0.0.19

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.
@@ -1,4 +1,4 @@
1
- import { getVercelOidcTokenSync } from "@vercel/oidc";
1
+ import { getVercelOidcToken } from "@vercel/oidc";
2
2
  import { decodeBase64Url } from "./decode-base64-url";
3
3
  import { z } from "zod";
4
4
 
@@ -28,13 +28,13 @@ export interface Credentials {
28
28
  * If both methods are used, the object properties take precedence over the
29
29
  * environment variable. If neither method is used, an error is thrown.
30
30
  */
31
- export function getCredentials<T>(params?: T | Credentials): Credentials {
31
+ export async function getCredentials(params?: unknown): Promise<Credentials> {
32
32
  const credentials = getCredentialsFromParams(params ?? {});
33
33
  if (credentials) {
34
34
  return credentials;
35
35
  }
36
36
 
37
- const oidcToken = getVercelOidcTokenSync();
37
+ const oidcToken = await getVercelOidcToken();
38
38
  if (oidcToken) {
39
39
  return getCredentialsFromOIDCToken(oidcToken);
40
40
  }
@@ -50,9 +50,12 @@ export function getCredentials<T>(params?: T | Credentials): Credentials {
50
50
  * required fields (`token`, `teamId`, and `projectId`) must be present
51
51
  * or none of them, otherwise an error is thrown.
52
52
  */
53
- function getCredentialsFromParams<Params extends Record<string, unknown>>(
54
- params: Params | Credentials,
55
- ): Credentials | null {
53
+ function getCredentialsFromParams(params: unknown): Credentials | null {
54
+ // Type guard: params must be an object
55
+ if (!params || typeof params !== "object") {
56
+ return null;
57
+ }
58
+
56
59
  const missing = [
57
60
  "token" in params && typeof params.token === "string" ? null : "token",
58
61
  "teamId" in params && typeof params.teamId === "string" ? null : "teamId",
@@ -63,9 +66,9 @@ function getCredentialsFromParams<Params extends Record<string, unknown>>(
63
66
 
64
67
  if (missing.length === 0) {
65
68
  return {
66
- token: params.token as string,
67
- projectId: params.projectId as string,
68
- teamId: params.teamId as string,
69
+ token: (params as any).token,
70
+ projectId: (params as any).projectId,
71
+ teamId: (params as any).teamId,
69
72
  };
70
73
  }
71
74
 
@@ -84,7 +87,9 @@ function getCredentialsFromParams<Params extends Record<string, unknown>>(
84
87
  * Schema to validate the payload of the Vercel OIDC token where we expect
85
88
  * to find the `teamId` and `projectId`.
86
89
  */
87
- const schema = z.object({
90
+ export const schema = z.object({
91
+ exp: z.number().optional().describe("Expiry timestamp (seconds since epoch)"),
92
+ iat: z.number().optional().describe("Issued at timestamp"),
88
93
  owner_id: z.string(),
89
94
  project_id: z.string(),
90
95
  });
@@ -0,0 +1,125 @@
1
+ import { assert, beforeEach, describe, expect, test, vi } from "vitest";
2
+ import { JwtExpiry } from "./jwt-expiry";
3
+ import type { getVercelOidcToken } from "@vercel/oidc";
4
+
5
+ const { getVercelOidcTokenMock } = vi.hoisted(() => {
6
+ return {
7
+ getVercelOidcTokenMock: vi.fn<typeof getVercelOidcToken>(),
8
+ };
9
+ });
10
+ vi.mock("@vercel/oidc", () => ({
11
+ getVercelOidcToken: getVercelOidcTokenMock,
12
+ }));
13
+ beforeEach(() => {
14
+ getVercelOidcTokenMock.mockReset();
15
+ });
16
+
17
+ describe("JwtExpiry", () => {
18
+ test("refreshes a token", async () => {
19
+ const token = createMockJWT({
20
+ owner_id: "team1",
21
+ project_id: "proj1",
22
+ });
23
+ getVercelOidcTokenMock.mockImplementationOnce(async () => "hello world");
24
+ const expiry = await JwtExpiry.fromToken(token)?.refresh();
25
+ expect(expiry).toBeInstanceOf(JwtExpiry);
26
+ expect(expiry?.token).toEqual("hello world");
27
+ });
28
+
29
+ test("isValid returns true for tokens without expiry", () => {
30
+ // Mock token without exp field (like OIDC tokens without exp)
31
+ const tokenWithoutExp = createMockJWT({
32
+ owner_id: "team1",
33
+ project_id: "proj1",
34
+ });
35
+ const expiry = JwtExpiry.fromToken(tokenWithoutExp);
36
+ assert(expiry, "Expiry should not be null for valid JWT");
37
+ expect(expiry.isValid()).toBe(false); // No exp field means malformed JWT
38
+ });
39
+
40
+ test("isValid returns true for unexpired tokens", () => {
41
+ const futureTime = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now
42
+ const tokenValid = createMockJWT({
43
+ owner_id: "team1",
44
+ project_id: "proj1",
45
+ exp: futureTime,
46
+ });
47
+ const expiry = JwtExpiry.fromToken(tokenValid);
48
+ assert(expiry, "Expiry should not be null for valid JWT");
49
+ expect(expiry.isValid()).toBe(true);
50
+ });
51
+
52
+ test("isValid returns false for expired tokens", () => {
53
+ const pastTime = Math.floor(Date.now() / 1000) - 3600; // 1 hour ago
54
+ const tokenExpired = createMockJWT({
55
+ owner_id: "team1",
56
+ project_id: "proj1",
57
+ exp: pastTime,
58
+ });
59
+ const expiry = JwtExpiry.fromToken(tokenExpired);
60
+ assert(expiry, "Expiry should not be null for valid JWT");
61
+ expect(expiry.isValid()).toBe(false);
62
+ });
63
+
64
+ test("isValid returns false for tokens expiring within buffer time", () => {
65
+ const soonTime = Math.floor(Date.now() / 1000) + 120; // 2 minutes from now
66
+ const tokenExpiringSoon = createMockJWT({
67
+ owner_id: "team1",
68
+ project_id: "proj1",
69
+ exp: soonTime,
70
+ });
71
+ const expiry = JwtExpiry.fromToken(tokenExpiringSoon);
72
+ assert(expiry, "Expiry should not be null for valid JWT");
73
+ expect(expiry.isValid(5)).toBe(false); // 5 minute buffer
74
+ });
75
+
76
+ test("isValid returns false for malformed JWT tokens", () => {
77
+ const expiry = JwtExpiry.fromToken("header.invalid-payload.signature");
78
+ assert(expiry, "Expiry should not be null for valid JWT");
79
+ expect(expiry.isValid()).toBe(false);
80
+ });
81
+
82
+ test("getExpiryDate returns correct expiry date", () => {
83
+ const expTime = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now
84
+ const token = createMockJWT({
85
+ owner_id: "team1",
86
+ project_id: "proj1",
87
+ exp: expTime,
88
+ });
89
+ const expiry = JwtExpiry.fromToken(token);
90
+ assert(expiry, "Expiry should not be null for valid JWT");
91
+ expect(expiry.getExpiryDate()).toEqual(new Date(expTime * 1000));
92
+ });
93
+
94
+ test("getExpiryDate returns null for tokens without expiry", () => {
95
+ const token = createMockJWT({ owner_id: "team1", project_id: "proj1" });
96
+ const expiry = JwtExpiry.fromToken(token);
97
+ assert(expiry, "Expiry should not be null for valid JWT");
98
+ expect(expiry.getExpiryDate()).toBeNull();
99
+ });
100
+
101
+ test("getExpiryDate returns null for malformed tokens", () => {
102
+ const token = "hello.world.hey";
103
+ const expiry = JwtExpiry.fromToken(token);
104
+ assert(expiry, "Expiry should not be null for valid JWT");
105
+ expect(expiry.getExpiryDate()).toBeNull();
106
+ });
107
+
108
+ test("returns null for non-JWT style tokens", () => {
109
+ expect(JwtExpiry.fromToken("personal-access-token")).toBeNull();
110
+ });
111
+ });
112
+
113
+ // Helper function to create mock JWT tokens for testing
114
+ function createMockJWT(payload: any): string {
115
+ const header = { typ: "JWT", alg: "HS256" };
116
+ const encodedHeader = Buffer.from(JSON.stringify(header)).toString(
117
+ "base64url",
118
+ );
119
+ const encodedPayload = Buffer.from(JSON.stringify(payload)).toString(
120
+ "base64url",
121
+ );
122
+ const signature = "mock-signature";
123
+
124
+ return `${encodedHeader}.${encodedPayload}.${signature}`;
125
+ }
@@ -0,0 +1,105 @@
1
+ import { z } from "zod";
2
+ import { decodeBase64Url } from "./decode-base64-url";
3
+ import { schema } from "./get-credentials";
4
+ import { getVercelOidcToken } from "@vercel/oidc";
5
+ import ms from "ms";
6
+
7
+ /** Time buffer before token expiry to consider it invalid (in milliseconds) */
8
+ const BUFFER_MS = ms("5m");
9
+
10
+ export class OidcRefreshError extends Error {
11
+ name = "OidcRefreshError";
12
+ }
13
+
14
+ /**
15
+ * Expiry implementation for JWT tokens (OIDC tokens).
16
+ * Parses the JWT once and provides fast expiry validation.
17
+ */
18
+ export class JwtExpiry {
19
+ private expiryTime: number | null; // Unix timestamp in seconds
20
+ readonly payload?: Readonly<z.infer<typeof schema>>;
21
+
22
+ static fromToken(token: string): JwtExpiry | null {
23
+ if (!isJwtFormat(token)) {
24
+ return null;
25
+ } else {
26
+ return new JwtExpiry(token);
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Creates a new JWT expiry checker.
32
+ *
33
+ * @param token - The JWT token to parse
34
+ */
35
+ constructor(readonly token: string) {
36
+ try {
37
+ const tokenContents = token.split(".")[1];
38
+ this.payload = schema.parse(decodeBase64Url(tokenContents));
39
+ this.expiryTime = this.payload.exp || null;
40
+ } catch {
41
+ // Malformed token - treat as expired to trigger refresh
42
+ this.expiryTime = 0;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Checks if the JWT token is valid (not expired).
48
+ * @returns true if token is valid, false if expired or expiring soon
49
+ */
50
+ isValid(): boolean {
51
+ if (this.expiryTime === null) {
52
+ return false; // No expiry means malformed JWT
53
+ }
54
+
55
+ const now = Math.floor(Date.now() / 1000);
56
+ const buffer = BUFFER_MS / 1000;
57
+ return now + buffer < this.expiryTime;
58
+ }
59
+
60
+ /**
61
+ * Gets the expiry date of the JWT token.
62
+ *
63
+ * @returns Date object representing when the token expires, or null if no expiry
64
+ */
65
+ getExpiryDate(): Date | null {
66
+ return this.expiryTime ? new Date(this.expiryTime * 1000) : null;
67
+ }
68
+
69
+ /**
70
+ * Refreshes the JWT token by fetching a new OIDC token.
71
+ *
72
+ * @returns Promise resolving to a new JwtExpiry instance with fresh token
73
+ */
74
+ async refresh(): Promise<JwtExpiry> {
75
+ try {
76
+ const freshToken = await getVercelOidcToken();
77
+ return new JwtExpiry(freshToken);
78
+ } catch (cause) {
79
+ throw new OidcRefreshError("Failed to refresh OIDC token", {
80
+ cause,
81
+ });
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Refreshes the JWT token if it's expired or expiring soon.
87
+ */
88
+ async tryRefresh(): Promise<JwtExpiry | null> {
89
+ if (this.isValid()) {
90
+ return null; // Still valid, no need to refresh
91
+ }
92
+
93
+ return this.refresh();
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Checks if a token follows JWT format (has 3 parts separated by dots).
99
+ *
100
+ * @param token - The token to check
101
+ * @returns true if token appears to be a JWT, false otherwise
102
+ */
103
+ function isJwtFormat(token: string): boolean {
104
+ return token.split(".").length === 3;
105
+ }
@@ -0,0 +1,7 @@
1
+ import { expect, it } from "vitest";
2
+ import { getPrivateParams } from "./types";
3
+
4
+ it("getPrivateParams filters unknown params", async () => {
5
+ const result = getPrivateParams({ foo: 123, __someParam: "abc" });
6
+ expect(result).toEqual({ __someParam: "abc" });
7
+ });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Utility type that extends a type to accept private parameters.
3
+ *
4
+ * The private parameters can then be extracted out of the object using
5
+ * `getPrivateParams`.
6
+ */
7
+ export type WithPrivate<T> = T & {
8
+ [K in `__${string}`]?: unknown;
9
+ };
10
+
11
+ /**
12
+ * Extract private parameters out of an object.
13
+ */
14
+ export const getPrivateParams = (params?: object) => {
15
+ const privateEntries = Object.entries(params ?? {}).filter(([k]) =>
16
+ k.startsWith("__"),
17
+ );
18
+ return Object.fromEntries(privateEntries) as {
19
+ [K in keyof typeof params as K extends `__${string}`
20
+ ? K
21
+ : never]: (typeof params)[K];
22
+ };
23
+ };
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Autogenerated by inject-version.ts
2
- export const VERSION = "0.0.17";
2
+ export const VERSION = "0.0.19";
package/turbo.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": ["//"],
3
+ "tasks": {
4
+ "version:bump": {
5
+ "inputs": ["package.json"],
6
+ "outputs": ["src/version.ts"]
7
+ }
8
+ }
9
+ }